Merge pull request #63 from prowler-cloud/PRWLR-4917-Improving-Filtering-Impacts-the-whole-app

Big Refactor: Integrated React Hook Form, Improved UI Consistency and added new features
This commit is contained in:
Pablo Lara
2024-09-30 16:32:36 +02:00
committed by GitHub
57 changed files with 1539 additions and 623 deletions
+56 -36
View File
@@ -6,6 +6,10 @@ import { redirect } from "next/navigation";
import { auth } from "@/auth.config";
import { parseStringify } from "@/lib";
// Credentials for basic auth
const username = process.env.API_USERNAME;
const password = process.env.API_PASSWORD;
export const getProviders = async ({
page = 1,
query = "",
@@ -18,7 +22,7 @@ export const getProviders = async ({
if (isNaN(Number(page)) || page < 1) redirect("/providers");
const keyServer = process.env.API_BASE_URL;
const url = new URL(`${keyServer}/providers`);
const url = new URL(`${keyServer}/providers?sort=-inserted_at`);
if (page) url.searchParams.append("page[number]", page.toString());
if (query) url.searchParams.append("filter[search]", query);
@@ -35,6 +39,7 @@ export const getProviders = async ({
const providers = await fetch(url.toString(), {
headers: {
"X-Tenant-ID": `${tenantId}`,
Authorization: "Basic " + btoa(`${username}:${password}`),
},
});
const data = await providers.json();
@@ -59,6 +64,7 @@ export const getProvider = async (formData: FormData) => {
const providers = await fetch(url.toString(), {
headers: {
"X-Tenant-ID": `${tenantId}`,
Authorization: "Basic " + btoa(`${username}:${password}`),
},
});
const data = await providers.json();
@@ -86,6 +92,7 @@ export const updateProvider = async (formData: FormData) => {
method: "PATCH",
headers: {
"X-Tenant-ID": `${tenantId}`,
Authorization: "Basic " + btoa(`${username}:${password}`),
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
@@ -110,26 +117,32 @@ export const updateProvider = async (formData: FormData) => {
};
export const addProvider = async (formData: FormData) => {
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const provider = formData.get("provider");
const providerId = formData.get("id");
const alias = formData.get("alias");
const tenantId = session?.user.tenantId;
const providerType = formData.get("providerType");
const providerId = formData.get("providerId");
const providerAlias = formData.get("providerAlias");
const url = new URL(`${keyServer}/providers`);
try {
const response = await fetch(`${keyServer}/providers`, {
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`,
"X-Tenant-ID": `${tenantId}`,
Authorization: "Basic " + btoa(`${username}:${password}`),
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
data: {
type: "Provider",
attributes: {
provider: provider,
provider_id: providerId,
alias: alias,
provider: providerType,
uid: providerId,
alias: providerAlias,
},
},
}),
@@ -146,40 +159,20 @@ export const addProvider = async (formData: FormData) => {
};
export const checkConnectionProvider = async (formData: FormData) => {
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const tenantId = session?.user.tenantId;
const providerId = formData.get("id");
try {
const response = await fetch(
`${keyServer}/providers/${providerId}/connection`,
{
method: "POST",
headers: {
"X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`,
},
},
);
const data = await response.json();
revalidatePath("/providers");
return parseStringify(data);
} catch (error) {
return {
error: getErrorMessage(error),
};
}
};
export const deleteProvider = async (formData: FormData) => {
const keyServer = process.env.API_BASE_URL;
const providerId = formData.get("id");
const url = new URL(`${keyServer}/providers/${providerId}/connection`);
try {
const response = await fetch(`${keyServer}/providers/${providerId}`, {
method: "DELETE",
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`,
"X-Tenant-ID": `${tenantId}`,
Authorization: "Basic " + btoa(`${username}:${password}`),
},
});
const data = await response.json();
@@ -191,6 +184,33 @@ export const deleteProvider = async (formData: FormData) => {
};
}
};
export const deleteProvider = async (formData: FormData) => {
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const tenantId = session?.user.tenantId;
const providerId = formData.get("id");
const url = new URL(`${keyServer}/providers/${providerId}`);
try {
const response = await fetch(url.toString(), {
method: "DELETE",
headers: {
"X-Tenant-ID": `${tenantId}`,
Authorization: "Basic " + btoa(`${username}:${password}`),
},
});
const data = await response.json();
revalidatePath("/providers");
return parseStringify(data);
} catch (error) {
return {
error: getErrorMessage(error),
};
}
};
export const getErrorMessage = (error: unknown): string => {
let message: string;
+1 -1
View File
@@ -46,7 +46,7 @@ export default function RootLayout({
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
<div className="flex items-center h-dvh justify-center overflow-hidden">
<SidebarWrap />
<main className="container h-full flex-1 flex-col p-4 mb-auto overflow-y-auto">
<main className="container h-full flex-1 flex-col p-4 mb-auto overflow-y-auto no-scrollbar">
{children}
<Toaster />
</main>
+11 -12
View File
@@ -2,8 +2,8 @@ import { Spacer } from "@nextui-org/react";
import { Suspense } from "react";
import { getProviders } from "@/actions";
import { FilterControls } from "@/components/filters";
import { AddProviderModal } from "@/components/providers";
import { FilterControls, filtersProviders } from "@/components/filters";
import { AddProvider } from "@/components/providers";
import {
ColumnsProvider,
DataTableProvider,
@@ -22,18 +22,17 @@ export default async function Providers({
return (
<>
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
<Spacer y={4} />
<FilterControls search providers />
<FilterControls search providers date customFilters={filtersProviders} />
<Spacer y={4} />
<div className="flex flex-col items-end w-full">
<div className="flex space-x-6">
<AddProviderModal />
</div>
<Spacer y={6} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableProvider />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
</div>
<AddProvider />
<Spacer y={4} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableProvider />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
</>
);
}
-74
View File
@@ -1,74 +0,0 @@
"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> = ({
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 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>
);
};
@@ -28,9 +28,9 @@ export const CustomDatePicker = () => {
(date: any) => {
const params = new URLSearchParams(searchParams.toString());
if (date) {
params.set("filter[updated_at__lte]", date.toString());
params.set("filter[updated_at]", date.toString());
} else {
params.delete("filter[updated_at__lte]");
params.delete("filter[updated_at]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
@@ -59,7 +59,6 @@ export const CustomDatePicker = () => {
return (
<div className="flex flex-col md:gap-2 w-full">
<DatePicker
CalendarBottomContent={<div className="min-w-[380px]"></div>}
CalendarTopContent={
<ButtonGroup
fullWidth
@@ -89,7 +88,6 @@ export const CustomDatePicker = () => {
}}
value={value}
onChange={handleDateChange}
label="Scan date"
size="sm"
variant="flat"
/>
@@ -1,6 +1,6 @@
import { Input } from "@nextui-org/react";
import debounce from "lodash.debounce";
import { XCircle } from "lucide-react";
import { SearchIcon, XCircle } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react";
@@ -37,11 +37,11 @@ export const CustomSearchInput: React.FC = () => {
return (
<Input
variant="bordered"
variant="flat"
placeholder="Search..."
label="Search"
labelPlacement="inside"
labelPlacement="outside"
value={searchQuery}
startContent={<SearchIcon className="text-default-400" width={16} />}
onChange={(e) => {
const value = e.target.value;
setSearchQuery(value);
@@ -54,6 +54,7 @@ export const CustomSearchInput: React.FC = () => {
</button>
)
}
radius="sm"
size="sm"
/>
);
@@ -9,7 +9,7 @@ import {
CustomProviderInputAzure,
CustomProviderInputGCP,
CustomProviderInputKubernetes,
} from "./CustomProviderInputs";
} from "./custom-provider-inputs";
const dataInputsProvider = [
{
@@ -43,9 +43,9 @@ export const CustomSelectProvider = () => {
(value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set("filter[provider]", value);
params.set("filter[provider__in]", value);
} else {
params.delete("filter[provider]");
params.delete("filter[provider__in]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
@@ -53,21 +53,18 @@ export const CustomSelectProvider = () => {
);
useEffect(() => {
const providerFromUrl = searchParams.get("filter[provider]") || "";
const providerFromUrl = searchParams.get("filter[provider__in]") || "";
setSelectedProvider(providerFromUrl);
}, [searchParams]);
return (
<Select
items={dataInputsProvider}
label="Select a Provider"
// selectionMode="multiple"
// label="Select a Provider"
placeholder="Select a provider"
labelPlacement="inside"
labelPlacement="outside"
size="sm"
classNames={{
base: "w-full",
trigger: "h-12",
}}
onChange={(e) => {
const value = e.target.value;
setSelectedProvider(value);
+13
View File
@@ -0,0 +1,13 @@
export const filtersProviders = [
{
key: "provider__in",
labelCheckboxGroup: "Select a Provider",
values: ["aws", "gcp", "azure", "kubernetes"],
},
{
key: "connected",
labelCheckboxGroup: "Status provider",
values: ["false", "true"],
},
// Add more filter categories as needed
];
+74
View File
@@ -0,0 +1,74 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useState } from "react";
import { FilterControlsProps } from "@/types";
import { CrossIcon } from "../icons";
import { DataTableFilterCustom } from "../providers/table";
import { CustomButton } from "../ui/custom";
import { CustomCheckboxMutedFindings } from "./custo-checkbox-muted-findings";
import { CustomAccountSelection } from "./custom-account-selection";
import { CustomDatePicker } from "./custom-date-picker";
import { CustomRegionSelection } from "./custom-region-selection";
import { CustomSearchInput } from "./custom-search-input";
import { CustomSelectProvider } from "./custom-select-provider";
export const FilterControls: React.FC<FilterControlsProps> = ({
search = false,
providers = false,
date = false,
regions = false,
accounts = false,
mutedFindings = false,
customFilters = [],
}) => {
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="flex flex-col gap-4">
<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 && (
<CustomButton
className="w-fit"
onPress={clearAllFilters}
variant="dashed"
size="sm"
endContent={<CrossIcon size={24} />}
radius="sm"
>
Reset
</CustomButton>
)}
</div>
<DataTableFilterCustom filters={customFilters} />
</div>
);
};
+8 -7
View File
@@ -1,7 +1,8 @@
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";
export * from "./custo-checkbox-muted-findings";
export * from "./custom-account-selection";
export * from "./custom-date-picker";
export * from "./custom-provider-inputs";
export * from "./custom-region-selection";
export * from "./custom-select-provider";
export * from "./data-filters";
export * from "./filter-controls";
@@ -10,8 +10,7 @@ import {
import { ColumnDef } from "@tanstack/react-table";
import { VerticalDotsIcon } from "@/components/icons";
import { StatusBadge } from "@/components/ui";
import { SeverityBadge } from "@/components/ui";
import { SeverityBadge, StatusBadge } from "@/components/ui/table";
import { FindingProps } from "@/types";
const getFindingsAttributes = (row: { original: FindingProps }) => {
@@ -19,7 +19,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui";
} from "@/components/ui/table";
import { FindingProps } from "@/types";
interface DataTableFindingsProps<TData extends FindingProps, TValue> {
+205 -20
View File
@@ -305,28 +305,31 @@ export const WifiPendingIcon: React.FC<IconSvgProps> = ({
);
export const DeleteIcon: React.FC<IconSvgProps> = ({
size = 24,
width,
size,
height,
width,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="none"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 24 24"
width={size || width}
{...props}
>
<path
fill="currentColor"
d="M10 5h4a2 2 0 1 0-4 0M8.5 5a3.5 3.5 0 1 1 7 0h5.75a.75.75 0 0 1 0 1.5h-1.32l-1.17 12.111A3.75 3.75 0 0 1 15.026 22H8.974a3.75 3.75 0 0 1-3.733-3.389L4.07 6.5H2.75a.75.75 0 0 1 0-1.5zm2 4.75a.75.75 0 0 0-1.5 0v7.5a.75.75 0 0 0 1.5 0zM14.25 9a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-1.5 0v-7.5a.75.75 0 0 1 .75-.75m-7.516 9.467a2.25 2.25 0 0 0 2.24 2.033h6.052a2.25 2.25 0 0 0 2.24-2.033L18.424 6.5H5.576z"
/>
</svg>
);
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || 48}
viewBox="0 0 24 24"
width={size || width || 48}
aria-hidden="true"
{...props}
>
<g fill="none">
<path d="m12.593 23.258-.011.002-.071.035-.02.004-.014-.004-.071-.035q-.016-.005-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427q-.004-.016-.017-.018m.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093q.019.005.029-.008l.004-.014-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014-.034.614q.001.018.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01z" />
<path
fill="currentColor"
d="M14.28 2a2 2 0 0 1 1.897 1.368L16.72 5H20a1 1 0 1 1 0 2l-.003.071-.867 12.143A3 3 0 0 1 16.138 22H7.862a3 3 0 0 1-2.992-2.786L4.003 7.07 4 7a1 1 0 0 1 0-2h3.28l.543-1.632A2 2 0 0 1 9.721 2zm3.717 5H6.003l.862 12.071a1 1 0 0 0 .997.929h8.276a1 1 0 0 0 .997-.929zM10 10a1 1 0 0 1 .993.883L11 11v5a1 1 0 0 1-1.993.117L9 16v-5a1 1 0 0 1 1-1m4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1m.28-6H9.72l-.333 1h5.226z"
/>
</g>
</svg>
);
};
export const CheckIcon: React.FC<IconSvgProps> = ({
size = 24,
@@ -595,3 +598,185 @@ export const SuccessIcon: React.FC<IconSvgProps> = ({
/>
</svg>
);
export const ArrowUpIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || "1em"}
viewBox="0 0 12 12"
width={size || width || "1em"}
aria-hidden="true"
focusable="false"
role="presentation"
{...props}
>
<path
d="M3 7.5L6 4.5L9 7.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
);
};
export const ArrowDownIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || "1em"}
viewBox="0 0 12 12"
width={size || width || "1em"}
aria-hidden="true"
focusable="false"
role="presentation"
{...props}
>
<path
d="M3 4.5L6 7.5L9 4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
/>
</svg>
);
};
export const ChevronsLeftRightIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || 24}
viewBox="0 0 24 24"
width={size || width || 24}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="lucide lucide-chevrons-left-right ml-2 h-4 w-4 rotate-90"
{...props}
>
<path d="m9 7-5 5 5 5M15 7l5 5-5 5" />
</svg>
);
};
export const PlusCircleIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || 15}
viewBox="0 0 15 15"
width={size || width || 15}
className="mr-2 size-4"
{...props}
>
<path
d="M7.5.877a6.623 6.623 0 1 0 0 13.246A6.623 6.623 0 0 0 7.5.877ZM1.827 7.5a5.673 5.673 0 1 1 11.346 0 5.673 5.673 0 0 1-11.346 0ZM7.5 4a.5.5 0 0 1 .5.5V7h2.5a.5.5 0 1 1 0 1H8v2.5a.5.5 0 0 1-1 0V8H4.5a.5.5 0 0 1 0-1H7V4.5a.5.5 0 0 1 .5-.5Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
);
};
export const CustomFilterIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
height={size || height || 16}
width={size || width || 16}
viewBox="0 0 24 24"
{...props}
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M9.5 14a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm5-10a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z" />
<path strokeLinecap="round" d="M15 16.959h7m-13-10H2m0 10h2m18-10h-2" />
</g>
</svg>
);
};
export const SaveIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || 48}
viewBox="0 0 24 24"
width={size || width || 48}
aria-hidden="true"
{...props}
>
<path
d="m20.71 9.29l-6-6a1 1 0 0 0-.32-.21A1.1 1.1 0 0 0 14 3H6a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-8a1 1 0 0 0-.29-.71M9 5h4v2H9Zm6 14H9v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1Zm4-1a1 1 0 0 1-1 1h-1v-3a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v3H6a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V6.41l4 4Z"
fill="currentColor"
/>
</svg>
);
};
export const AddIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || 20}
viewBox="0 0 24 24"
width={size || width || 20}
aria-hidden="true"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m.75-13a.75.75 0 0 0-1.5 0v2.25H9a.75.75 0 0 0 0 1.5h2.25V15a.75.75 0 0 0 1.5 0v-2.25H15a.75.75 0 0 0 0-1.5h-2.25z"
clipRule="evenodd"
/>
</svg>
);
};
-63
View File
@@ -1,63 +0,0 @@
"use client";
import { useRef } from "react";
import { addProvider } from "@/actions";
import { useToast } from "../ui/toast";
import { ButtonAddProvider } from "./ButtonAddProvider";
export const AddProvider = () => {
const ref = useRef<HTMLFormElement>(null);
const { toast } = useToast();
async function clientAction(formData: FormData) {
// reset the form
ref.current?.reset();
// client-side validation
const data = await addProvider(formData);
if (data?.errors) {
data.errors.forEach((error: { detail: string }) => {
const errorMessage = `${error.detail}`;
// show error
toast({
variant: "destructive",
title: "Oops! Something went wrong",
description: errorMessage,
});
});
} else {
toast({
title: "Success!",
description: "The provider was added successfully.",
});
}
}
return (
<form ref={ref} action={clientAction} className="flex gap-x-2">
<input
type="text"
name="provider"
placeholder="Provider"
aria-label="Provider"
className="py-2 px-3 rounded-sm"
/>
<input
type="text"
name="id"
placeholder="Provider ID"
aria-label="Provider ID"
className="py-2 px-3 rounded-sm"
/>
<input
type="text"
name="alias"
placeholder="Alias"
aria-label="Alias"
className="py-2 px-3 rounded-sm"
/>
<ButtonAddProvider />
</form>
);
};
-102
View File
@@ -1,102 +0,0 @@
"use client";
import { Button, Input } from "@nextui-org/react";
import { useRef, useState } from "react";
import { addProvider } from "@/actions";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
useToast,
} from "@/components/ui";
import { ButtonAddProvider } from "./ButtonAddProvider";
import { CustomRadioProvider } from "./CustomRadioProvider";
export const AddProviderModal = () => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLFormElement>(null);
const { toast } = useToast();
async function clientAction(formData: FormData) {
// reset the form
ref.current?.reset();
// client-side validation
const data = await addProvider(formData);
if (data?.errors) {
data.errors.forEach((error: { detail: string }) => {
const errorMessage = `${error.detail}`;
// show error
toast({
variant: "destructive",
title: "Oops! Something went wrong",
description: errorMessage,
});
});
} else {
toast({
title: "Success!",
description: "The provider was added successfully.",
});
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button aria-label="Add Cloud Account" variant="ghost">
Add Cloud Account
</Button>
</DialogTrigger>
<DialogContent className="flex flex-col sm:max-w-md md:max-w-4xl">
<DialogHeader className="mb-6 space-y-3">
<DialogTitle className="text-2xl text-center">
Add cloud account
</DialogTitle>
<DialogDescription className="text-md">
You must manually deploy a new read-only IAM role for each account
you want to add. The following links will provide detailed
instructions how to do this:
</DialogDescription>
</DialogHeader>
<form
ref={ref}
action={clientAction}
onSubmit={() => setOpen(false)}
className="grid sm:grid-cols-2 gap-6"
>
<div className="col-span-1">
<CustomRadioProvider />
</div>
<div className="col-span-1 flex flex-col gap-y-2 my-auto">
<Input
type="text"
name="id"
label="Provider ID"
labelPlacement="outside"
placeholder="Provider ID"
className="w-full rounded-sm"
aria-label="Enter Provider ID"
/>
<Input
type="text"
name="alias"
label="Alias"
labelPlacement="outside"
placeholder="alias"
className="w-full rounded-sm"
aria-label="Enter Provider alias"
/>
</div>
<div className="col-span-2 flex justify-center mt-4">
<ButtonAddProvider />
</div>
</form>
</DialogContent>
</Dialog>
);
};
@@ -1,14 +0,0 @@
"use client";
import { Button, CircularProgress } from "@nextui-org/react";
import React from "react";
import { useFormStatus } from "react-dom";
export const ButtonAddProvider = () => {
const { pending } = useFormStatus();
return (
<Button type="submit" area-disabled={pending}>
{pending ? <CircularProgress aria-label="Loading..." /> : "Add provider"}
</Button>
);
};
+48 -21
View File
@@ -3,9 +3,15 @@
import { UseRadioProps } from "@nextui-org/radio/dist/use-radio";
import { cn, RadioGroup, useRadio, VisuallyHidden } from "@nextui-org/react";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { z } from "zod";
import { addProviderFormSchema } from "@/types";
import { AWSProviderBadge, AzureProviderBadge } from "../icons/providers-badge";
import { GCPProviderBadge } from "../icons/providers-badge/GCPProviderBadge";
import { KS8ProviderBadge } from "../icons/providers-badge/KS8ProviderBadge";
import { FormMessage } from "../ui/form";
interface CustomRadioProps extends UseRadioProps {
description?: string;
@@ -52,27 +58,48 @@ export const CustomRadio: React.FC<CustomRadioProps> = (props) => {
);
};
export const CustomRadioProvider = () => {
interface CustomRadioProviderProps {
control: Control<z.infer<typeof addProviderFormSchema>>;
}
export const CustomRadioProvider: React.FC<CustomRadioProviderProps> = ({
control,
}) => {
return (
<RadioGroup label="Select one provider" name="provider">
<CustomRadio description="Amazon Web Services" value="aws">
<div className="flex items-center">
<AWSProviderBadge size={26} />
<span className="ml-2">AWS</span>
</div>
</CustomRadio>
<CustomRadio description="Google Cloud Platform" value="gcp">
<div className="flex items-center">
<GCPProviderBadge size={26} />
<span className="ml-2">GCP</span>
</div>
</CustomRadio>
<CustomRadio description="Microsoft Azure" value="azure">
<div className="flex items-center">
<AzureProviderBadge size={26} />
<span className="ml-2">Azure</span>
</div>
</CustomRadio>
</RadioGroup>
<Controller
name="providerType"
control={control}
render={({ field }) => (
<>
<RadioGroup label="Select one provider" {...field}>
<CustomRadio description="Amazon Web Services" value="aws">
<div className="flex items-center">
<AWSProviderBadge size={26} />
<span className="ml-2">AWS</span>
</div>
</CustomRadio>
<CustomRadio description="Google Cloud Platform" value="gcp">
<div className="flex items-center">
<GCPProviderBadge size={26} />
<span className="ml-2">GCP</span>
</div>
</CustomRadio>
<CustomRadio description="Microsoft Azure" value="azure">
<div className="flex items-center">
<AzureProviderBadge size={26} />
<span className="ml-2">Azure</span>
</div>
</CustomRadio>
<CustomRadio description="Kubernetes" value="kubernetes">
<div className="flex items-center">
<KS8ProviderBadge size={26} />
<span className="ml-2">Kubernetes</span>
</div>
</CustomRadio>
</RadioGroup>
<FormMessage className="text-system-error dark:text-system-error" />
</>
)}
/>
);
};
+4 -1
View File
@@ -12,6 +12,7 @@ export const SnippetIdProvider: React.FC<SnippetIdProviderProps> = ({
return (
<Snippet
className="flex items-center py-0"
color="default"
size="sm"
variant="flat"
radius="lg"
@@ -21,7 +22,9 @@ export const SnippetIdProvider: React.FC<SnippetIdProviderProps> = ({
>
<p className="flex items-center space-x-2">
<IdIcon size={16} />
<span className="text-sm max-w-24 overflow-x-scroll">{providerId}</span>
<span className="text-sm max-w-24 overflow-x-scroll no-scrollbar">
{providerId}
</span>
</p>
</Snippet>
);
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { useState } from "react";
import { AddIcon } from "../icons";
import { CustomAlertModal, CustomButton } from "../ui/custom";
import { AddForm } from "./forms";
export const AddProvider = () => {
const [isAddOpen, setIsAddOpen] = useState(false);
return (
<>
<CustomAlertModal
isOpen={isAddOpen}
onOpenChange={setIsAddOpen}
title="Add Cloud Provider"
description={
"You must manually deploy a new read-only IAM role for each account you want to add. The following links will provide detailed instructions how to do this:"
}
>
<AddForm setIsOpen={setIsAddOpen} />
</CustomAlertModal>
<div className="w-full flex items-center justify-end">
<CustomButton
variant="solid"
color="action"
size="md"
onPress={() => setIsAddOpen(true)}
endContent={<AddIcon size={20} />}
>
Add Account
</CustomButton>
</div>
</>
);
};
+122
View File
@@ -0,0 +1,122 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Dispatch, SetStateAction } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { addProvider } from "@/actions";
import { SaveIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
import { CustomButton, CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { addProviderFormSchema } from "@/types";
import { CustomRadioProvider } from "../CustomRadioProvider";
export const AddForm = ({
setIsOpen,
}: {
setIsOpen: Dispatch<SetStateAction<boolean>>;
}) => {
const formSchema = addProviderFormSchema;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerType: "",
providerId: "",
providerAlias: "",
},
});
const { toast } = useToast();
const isLoading = form.formState.isSubmitting;
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
const formData = new FormData();
Object.entries(values).forEach(
([key, value]) => value !== undefined && formData.append(key, value),
);
const data = await addProvider(formData);
if (data?.errors && data.errors.length > 0) {
const error = data.errors[0];
const errorMessage = `${error.detail}`;
// show error
toast({
variant: "destructive",
title: "Oops! Something went wrong",
description: errorMessage,
});
} else {
toast({
title: "Success!",
description: "The provider was updated successfully.",
});
setIsOpen(false); // Close the modal on success
}
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<CustomRadioProvider control={form.control} />
<CustomInput
control={form.control}
name="providerId"
type="text"
label="Provider ID"
labelPlacement="inside"
placeholder={"Enter the provider ID"}
variant="bordered"
isRequired
isInvalid={!!form.formState.errors.providerId}
/>
<CustomInput
control={form.control}
name="providerAlias"
type="text"
label="Alias"
labelPlacement="inside"
placeholder={"Enter the provider alias"}
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.providerAlias}
/>
<div className="w-full flex justify-center sm:space-x-6">
<CustomButton
type="button"
className="w-full bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={() => setIsOpen(false)}
disabled={isLoading}
>
<span>Cancel</span>
</CustomButton>
<CustomButton
type="submit"
className="w-full"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <SaveIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Confirm</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
+22 -22
View File
@@ -1,13 +1,14 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button, CircularProgress } from "@nextui-org/react";
import React, { Dispatch, SetStateAction } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { deleteProvider } from "@/actions";
import { DeleteIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
import { CustomButton } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
const formSchema = z.object({
@@ -46,6 +47,7 @@ export const DeleteForm = ({
description: "The provider was removed successfully.",
});
}
setIsOpen(false); // Close the modal on success
}
return (
@@ -53,31 +55,29 @@ export const DeleteForm = ({
<form action={onSubmitClient}>
<input type="hidden" name="id" value={providerId} />
<div className="w-full flex justify-center sm:space-x-6">
<Button
size="lg"
variant="bordered"
disabled={isLoading}
className="w-full hidden sm:block"
<CustomButton
type="button"
onPress={() => setIsOpen(false)}
>
Cancel
</Button>
<Button
className="w-full bg-transparent"
variant="faded"
size="lg"
type="submit"
radius="lg"
onPress={() => setIsOpen(false)}
disabled={isLoading}
className="w-full bg-system-error hover:bg-system-error/90 text-white"
>
{isLoading ? (
<>
<CircularProgress aria-label="Loading..." size="sm" />
Deleting
</>
) : (
<span>Delete</span>
)}
</Button>
<span>Cancel</span>
</CustomButton>
<CustomButton
type="submit"
className="w-full"
variant="solid"
color="danger"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <DeleteIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Delete</span>}
</CustomButton>
</div>
</form>
</Form>
+23 -22
View File
@@ -1,14 +1,14 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button, CircularProgress } from "@nextui-org/react";
import React, { Dispatch, SetStateAction } from "react";
import { Dispatch, SetStateAction } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { updateProvider } from "@/actions";
import { SaveIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
import { CustomInput } from "@/components/ui/custom";
import { CustomButton, CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { editProviderFormSchema } from "@/types";
@@ -32,6 +32,7 @@ export const EditForm = ({
});
const { toast } = useToast();
const isLoading = form.formState.isSubmitting;
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
@@ -86,29 +87,29 @@ export const EditForm = ({
<input type="hidden" name="providerId" value={providerId} />
<div className="w-full flex justify-center sm:space-x-6">
<Button
size="lg"
variant="bordered"
disabled={isLoading}
className="w-full hidden sm:block"
<CustomButton
type="button"
onPress={() => setIsOpen(false)}
>
Cancel
</Button>
<Button
className="w-full bg-transparent"
variant="faded"
size="lg"
type="submit"
radius="lg"
onPress={() => setIsOpen(false)}
disabled={isLoading}
className="w-full"
>
{isLoading ? (
<CircularProgress aria-label="Loading..." size="md" />
) : (
<span>Save</span>
)}
</Button>
<span>Cancel</span>
</CustomButton>
<CustomButton
type="submit"
className="w-full"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <SaveIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Save</span>}
</CustomButton>
</div>
</form>
</Form>
+1
View File
@@ -1,2 +1,3 @@
export * from "./AddForm";
export * from "./DeleteForm";
export * from "./EditForm";
+1 -3
View File
@@ -1,6 +1,4 @@
export * from "./AddProvider";
export * from "./AddProviderModal";
export * from "./ButtonAddProvider";
export * from "./add-provider";
export * from "./CheckConnectionProvider";
export * from "./CustomRadioProvider";
export * from "./DateWithTime";
@@ -3,7 +3,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { add } from "date-fns";
import { StatusBadge } from "@/components/ui";
import { StatusBadge } from "@/components/ui/table";
import { ProviderProps } from "@/types";
import { DateWithTime } from "../DateWithTime";
@@ -40,15 +40,15 @@ export const ColumnsProvider: ColumnDef<ProviderProps>[] = [
},
},
{
accessorKey: "id",
accessorKey: "uid",
header: ({ column }) => (
<DataTableColumnHeader column={column} title={"Id"} param="provider_id" />
),
cell: ({ row }) => {
const {
attributes: { provider_id },
attributes: { uid },
} = getProviderData(row);
return <SnippetIdProvider providerId={provider_id} />;
return <SnippetIdProvider providerId={uid} />;
},
},
{
@@ -2,13 +2,14 @@
import { Button } from "@nextui-org/react";
import { Column } from "@tanstack/react-table";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { HTMLAttributes } from "react";
import {
ArrowDownIcon,
ArrowUpIcon,
ChevronsLeftRightIcon,
} from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { HTMLAttributes } from "react";
} from "@/components/icons";
interface DataTableColumnHeaderProps<TData, TValue>
extends HTMLAttributes<HTMLDivElement> {
@@ -64,12 +65,12 @@ export const DataTableColumnHeader = <TData, TValue>({
currentSortParam === "" ||
(currentSortParam !== param && currentSortParam !== `-${param}`)
) {
return <ChevronsLeftRightIcon className="ml-2 h-4 w-4 rotate-90" />;
return <ChevronsLeftRightIcon size={14} className="ml-2 rotate-90" />;
}
return currentSortParam === `-${param}` ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
<ArrowDownIcon size={12} className="ml-2" />
) : (
<ArrowUpIcon className="ml-2 h-4 w-4" />
<ArrowUpIcon size={12} className="ml-2" />
);
};
@@ -79,9 +80,7 @@ export const DataTableColumnHeader = <TData, TValue>({
return (
<Button
variant="light"
size="md"
className="text-slate-500 dark:text-slate-400 h-8"
className="w-full justify-between px-0 text-left align-middle dark:text-slate-400 h-10 bg-transparent whitespace-nowrap text-foreground-500 text-tiny font-semibold outline-none"
onClick={getToggleSortingHandler}
>
<span>{title}</span>
@@ -1,40 +1,34 @@
"use client";
import { Button } from "@nextui-org/react";
import { Divider } from "@nextui-org/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo } from "react";
import React, { useState } from "react";
import { useCallback } from "react";
interface DataTableFilterCustomProps {
filters: { key: string; values: string[] }[];
import { CustomFilterIcon } from "@/components/icons";
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
import { FilterOption } from "@/types";
export interface DataTableFilterCustomProps {
filters: FilterOption[];
}
export function DataTableFilterCustom({ filters }: DataTableFilterCustomProps) {
export const DataTableFilterCustom = ({
filters,
}: DataTableFilterCustomProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [showFilters, setShowFilters] = useState(false);
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)) {
// eslint-disable-next-line security/detect-object-injection
currentFilters[filterKey] = value;
}
}
});
return currentFilters;
}, [searchParams, filters]);
const applyFilter = useCallback(
(key: string, value: string) => {
const pushDropdownFilter = useCallback(
(key: string, values: string[]) => {
const params = new URLSearchParams(searchParams);
const filterKey = `filter[${key}]`;
if (params.get(filterKey) === value) {
if (values.length === 0) {
params.delete(filterKey);
} else {
params.set(filterKey, value);
params.set(filterKey, values.join(","));
}
router.push(`?${params.toString()}`);
@@ -43,20 +37,41 @@ export function DataTableFilterCustom({ filters }: DataTableFilterCustomProps) {
);
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)}
// eslint-disable-next-line security/detect-object-injection
variant={activeFilters[key] === value ? "faded" : "light"}
size="sm"
>
{value || "All"}
</Button>
)),
)}
<div className="flex flex-row items-center gap-4">
<CustomButton
variant="flat"
color={showFilters ? "action" : "primary"}
size="sm"
startContent={<CustomFilterIcon size={16} />}
onPress={() => setShowFilters(!showFilters)}
>
{showFilters ? "Hide Filters" : "Show Filters"}
</CustomButton>
<div
className={`transition-all duration-500 ease-in-out ${
showFilters
? "opacity-100 max-h-96 overflow-visible"
: "opacity-0 max-h-0 overflow-hidden"
}`}
>
<div className="flex flex-row gap-4">
{filters.map((filter) => (
<CustomDropdownFilter
key={filter.key}
filter={{
...filter,
labelCheckboxGroup: filter.labelCheckboxGroup,
}}
onFilterChange={pushDropdownFilter}
/>
))}
<div className="flex flex-row items-center gap-2">
<Divider className="text-default-800 h-5" orientation="vertical" />
<span className="text-sm text-default-800">Selected</span>
</div>
</div>
</div>
</div>
);
}
};
@@ -9,7 +9,7 @@ import {
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { extractPaginationInfo } from "@/lib";
import { getPaginationInfo } from "@/lib";
import { MetaDataProps } from "@/types";
interface DataTablePaginationProps {
@@ -22,8 +22,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const { currentPage, totalPages, totalEntries } =
extractPaginationInfo(metadata);
const { currentPage, totalPages, totalEntries } = getPaginationInfo(metadata);
const createPageUrl = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
@@ -20,10 +20,9 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui";
} from "@/components/ui/table";
import { MetaDataProps } from "@/types";
import { DataTableFilterCustom } from "./data-table-filter-custom";
import { DataTablePagination } from "./data-table-pagination";
interface DataTableProviderProps<TData, TValue> {
@@ -55,20 +54,9 @@ export function DataTableProvider<TData, TValue>({
},
});
// This will be used to have "custom" filters in the table and will be
// passed to the DataTableFilterCustom 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 (
<>
<DataTableFilterCustom filters={filters} />
<div className="rounded-md border w-full">
<div className="p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto rounded-large shadow-small w-full ">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
+96
View File
@@ -0,0 +1,96 @@
import { Button, CircularProgress } from "@nextui-org/react";
import type { PressEvent } from "@react-types/shared";
import clsx from "clsx";
import { NextUIColors, NextUIVariants } from "@/types";
export const buttonClasses = {
base: "px-2 inline-flex items-center justify-center relative z-0 text-center whitespace-nowrap",
primary: "bg-default-100 hover:bg-default-200 text-default-800",
secondary: "bg-prowler-grey-light dark:bg-prowler-grey-medium text-white",
action: "text-white bg-prowler-blue-smoky dark:bg-prowler-grey-medium",
dashed:
"border border-default border-dashed bg-transparent justify-center whitespace-nowrap font-medium shadow-sm hover:border-solid hover:bg-default-100 active:bg-default-200 active:border-solid",
transparent: "border-0 border-transparent bg-transparent",
disabled: "pointer-events-none opacity-40",
hover: "hover:shadow-md",
};
interface ButtonProps {
type?: "button" | "submit" | "reset";
className?: string;
variant?:
| "solid"
| "faded"
| "bordered"
| "light"
| "flat"
| "ghost"
| "dashed"
| "shadow";
color?:
| "primary"
| "secondary"
| "action"
| "success"
| "warning"
| "danger"
| "transparent";
onPress?: (e: PressEvent) => void;
children: React.ReactNode;
startContent?: React.ReactNode;
endContent?: React.ReactNode;
size?: "sm" | "md" | "lg";
radius?: "none" | "sm" | "md" | "lg" | "full";
dashed?: boolean;
disabled?: boolean;
isLoading?: boolean;
isIconOnly?: boolean;
}
export const CustomButton = ({
type = "button",
className,
variant = "solid",
color = "primary",
onPress,
children,
startContent,
endContent,
size = "md",
radius = "sm",
disabled = false,
isLoading = false,
isIconOnly,
...props
}: ButtonProps) => (
<Button
type={type}
onPress={onPress}
variant={variant as NextUIVariants}
color={color as NextUIColors}
className={clsx(
buttonClasses.base,
{
[buttonClasses.primary]: color === "primary",
[buttonClasses.secondary]: color === "secondary",
[buttonClasses.action]: color === "action",
[buttonClasses.dashed]: variant === "dashed",
[buttonClasses.transparent]: color === "transparent",
[buttonClasses.disabled]: disabled,
[buttonClasses.hover]: color !== "transparent" && !disabled,
},
className,
)}
startContent={startContent}
endContent={endContent}
size={size}
radius={radius}
spinner={<CircularProgress aria-label="Loading..." size="sm" />}
isLoading={isLoading}
isIconOnly={isIconOnly}
{...props}
>
{children}
</Button>
);
@@ -0,0 +1,167 @@
"use client";
import {
Badge,
Button,
Checkbox,
CheckboxGroup,
Divider,
Popover,
PopoverContent,
PopoverTrigger,
} from "@nextui-org/react";
import { useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { PlusCircleIcon } from "@/components/icons";
import { CustomDropdownFilterProps } from "@/types";
export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
filter,
onFilterChange,
}) => {
const searchParams = useSearchParams();
const [groupSelected, setGroupSelected] = useState(new Set<string>());
const allFilterKeys = filter?.values || [];
const getActiveFilter = 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 (filter && filter.key === filterKey) {
// eslint-disable-next-line security/detect-object-injection
currentFilters[filterKey] = value;
}
}
});
return currentFilters;
}, [searchParams, filter]);
useEffect(() => {
if (filter && getActiveFilter[filter.key]) {
const activeValues = getActiveFilter[filter.key].split(",");
const newSelection = new Set(activeValues);
if (newSelection.size === allFilterKeys.length) {
newSelection.add("all");
}
setGroupSelected(newSelection);
} else {
setGroupSelected(new Set());
}
}, [getActiveFilter, filter, allFilterKeys]);
const onSelectionChange = useCallback(
(keys: string[]) => {
const newSelection = new Set(keys);
if (
newSelection.size === allFilterKeys.length &&
!newSelection.has("all")
) {
setGroupSelected(new Set(["all", ...allFilterKeys]));
} else if (groupSelected.has("all")) {
newSelection.delete("all");
const remainingValues = allFilterKeys.filter((key) =>
newSelection.has(key),
);
setGroupSelected(new Set(remainingValues));
} else {
setGroupSelected(newSelection);
}
if (onFilterChange && filter) {
const selectedValues = Array.from(newSelection).filter(
(key) => key !== "all",
);
onFilterChange(filter.key, selectedValues);
}
},
[allFilterKeys, groupSelected, onFilterChange, filter],
);
const handleSelectAllClick = useCallback(() => {
if (groupSelected.has("all")) {
setGroupSelected(new Set());
} else {
setGroupSelected(new Set(["all", ...allFilterKeys]));
}
}, [groupSelected, allFilterKeys]);
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<Popover placement="bottom">
<PopoverTrigger>
<Button
className="bg-default-100 text-default-800"
startContent={
<PlusCircleIcon className="text-default-400" width={16} />
}
size="sm"
>
{filter?.labelCheckboxGroup}
{groupSelected.size > 0 && (
<>
<Divider orientation="vertical" className="mx-2 h-4" />
<Badge
variant="flat"
className="rounded-sm px-1 font-normal lg:hidden"
>
{groupSelected.size}
</Badge>
<div className="hidden space-x-1 lg:flex">
{groupSelected.size > 2 ? (
<Badge
variant="flat"
className="rounded-sm px-1 font-normal"
>
{groupSelected.size} selected
</Badge>
) : (
Array.from(groupSelected)
.filter((value) => value !== "all")
.map((value) => (
<Badge
variant="flat"
key={value}
className="rounded-sm px-1 font-normal"
>
{value}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="flex w-full flex-col gap-6 px-2 py-4">
<CheckboxGroup
label={filter?.labelCheckboxGroup}
value={Array.from(groupSelected)}
onValueChange={onSelectionChange}
>
<Checkbox value="all" onValueChange={handleSelectAllClick}>
Select All
</Checkbox>
{allFilterKeys.map((value) => (
<Checkbox key={value} value={value}>
{value}
</Checkbox>
))}
</CheckboxGroup>
</div>
</PopoverContent>
</Popover>
{groupSelected?.size > 0 && (
<p className="text-small text-default-500">
Selected:{" "}
{Array.from(groupSelected)
.filter((item) => item !== "all")
.join(", ")}
</p>
)}
</div>
);
};
+2
View File
@@ -1,5 +1,7 @@
export * from "./custom-dropdown-filter";
export * from "./CustomAlertModal";
export * from "./CustomBox";
export * from "./CustomButton";
export * from "./CustomButtonClientAction";
export * from "./CustomInput";
export * from "./CustomLoader";
@@ -1,7 +1,7 @@
"use client";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { Cross2Icon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -38,14 +38,14 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-slate-800 dark:bg-slate-950",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 data-[state=open]:text-slate-500 dark:ring-offset-slate-950 dark:focus:ring-slate-300 dark:data-[state=open]:bg-slate-800 dark:data-[state=open]:text-slate-400">
<X className="h-4 w-4" />
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
@@ -102,7 +102,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-slate-500 dark:text-slate-400", className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
+1 -4
View File
@@ -2,13 +2,10 @@ export * from "./action-card/ActionCard";
export * from "./alert/Alert";
export * from "./alert-dialog/AlertDialog";
export * from "./chart/Chart";
export * from "./dialog/Dialog";
export * from "./dialog/dialog";
export * from "./dropdown/Dropdown";
export * from "./header/Header";
export * from "./label/Label";
export * from "./select/Select";
export * from "./sidebar";
export * from "./table/SeverityBadge";
export * from "./table/StatusBadge";
export * from "./table/Table";
export * from "./toast";
+2 -2
View File
@@ -42,7 +42,7 @@ export const SidebarWrap = () => {
return (
<div
className={clsx(
"relative flex h-screen flex-col !border-r-small border-divider transition-width",
"relative flex h-screen flex-col !border-r-small border-divider transition-width bg-prowler-blue-midnight",
{
"w-72 p-6": !isCompact,
"w-16 items-center px-2 py-6": isCompact,
@@ -79,7 +79,7 @@ export const SidebarWrap = () => {
/>{" "}
</Link>
<ScrollShadow className="-mr-6 h-full max-h-full py-6 pr-6">
<ScrollShadow hideScrollBar className="-mr-6 h-full max-h-full py-6 pr-6">
<Sidebar
defaultSelectedKey="overview"
isCompact={isCompact}
+3
View File
@@ -0,0 +1,3 @@
export * from "./severity-badge";
export * from "./status-badge";
export * from "./table";
@@ -1,12 +1,11 @@
import { cn } from "@nextui-org/react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<div className="relative w-full overflow-auto rounded-lg">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
@@ -20,7 +19,14 @@ const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
<thead
ref={ref}
className={cn(
"[&>tr]:first:rounded-lg [&>tr]:first:shadow-small",
className,
)}
{...props}
/>
));
TableHeader.displayName = "TableHeader";
@@ -28,11 +34,7 @@ const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
<tbody ref={ref} className={cn("", className)} {...props} />
));
TableBody.displayName = "TableBody";
@@ -43,7 +45,7 @@ const TableFooter = React.forwardRef<
<tfoot
ref={ref}
className={cn(
"border-t bg-slate-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-slate-800/50",
"font-medium [&>tr]:last:border-b-0 dark:bg-slate-800/50",
className,
)}
{...props}
@@ -58,7 +60,7 @@ const TableRow = React.forwardRef<
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-slate-100/50 data-[state=selected]:bg-slate-100 dark:hover:bg-slate-800/50 dark:data-[state=selected]:bg-slate-800",
"transition-colors hover:bg-slate-100/50 data-[state=selected]:bg-slate-100 dark:hover:bg-slate-800/50 dark:data-[state=selected]:bg-slate-800",
className,
)}
{...props}
@@ -73,7 +75,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-slate-500 [&:has([role=checkbox])]:pr-0 dark:text-slate-400",
"px-4 text-left align-middle bg-default-100 [&:has([role=checkbox])]:pr-0 dark:text-slate-400 h-10 rtl:text-right whitespace-nowrap text-foreground-500 text-tiny font-semibold first:rounded-l-lg rtl:first:rounded-r-lg rtl:first:rounded-l-[unset] last:rounded-r-lg rtl:last:rounded-l-lg rtl:last:rounded-r-[unset] data-[hover=true]:text-foreground-400 outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2",
className,
)}
{...props}
@@ -87,7 +89,10 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn(
"px-4 py-2.5 align-middle [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
));
@@ -99,7 +104,7 @@ const TableCaption = React.forwardRef<
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-slate-500 dark:text-slate-400", className)}
className={cn("mt-2 text-sm text-slate-500 dark:text-slate-400", className)}
{...props}
/>
));
+1 -1
View File
@@ -3,7 +3,7 @@
import { ColumnDef } from "@tanstack/react-table";
import { DateWithTime } from "@/components/providers";
import { StatusBadge } from "@/components/ui";
import { StatusBadge } from "@/components/ui/table";
import { UserActions } from "@/components/users";
import { UserProps } from "@/types";
@@ -9,7 +9,7 @@ import {
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { extractPaginationInfo } from "@/lib";
import { getPaginationInfo } from "@/lib";
import { MetaDataProps } from "@/types";
interface DataTablePaginationProps {
@@ -22,8 +22,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const { currentPage, totalPages, totalEntries } =
extractPaginationInfo(metadata);
const { currentPage, totalPages, totalEntries } = getPaginationInfo(metadata);
const createPageUrl = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
+1 -1
View File
@@ -18,7 +18,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui";
} from "@/components/ui/table";
import { MetaDataProps } from "@/types";
import { DataTablePagination } from "./DataTablePagination";
+21
View File
@@ -0,0 +1,21 @@
import { MetaDataProps } from "@/types";
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
export const getPaginationInfo = (metadata: MetaDataProps) => {
const currentPage = metadata?.pagination.page ?? "1";
const totalPages = metadata?.pagination.pages;
const totalEntries = metadata?.pagination.count;
return { currentPage, totalPages, totalEntries };
};
export function encryptKey(passkey: string) {
return btoa(passkey);
}
export function decryptKey(passkey: string) {
return atob(passkey);
}
+1
View File
@@ -1,2 +1,3 @@
export * from "./custom";
export * from "./seed";
export * from "./utils";
+1 -104
View File
@@ -1,109 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { extendTailwindMerge } from "tailwind-merge";
const COMMON_UNITS = ["small", "medium", "large"];
import { MetaDataProps } from "@/types";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const twMerge = extendTailwindMerge({
extend: {
theme: {
opacity: ["disabled"],
spacing: ["divider"],
borderWidth: COMMON_UNITS,
borderRadius: COMMON_UNITS,
},
classGroups: {
shadow: [{ shadow: COMMON_UNITS }],
"font-size": [{ text: ["tiny", ...COMMON_UNITS] }],
"bg-image": ["bg-stripe-gradient"],
},
},
});
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
export const extractPaginationInfo = (metadata: MetaDataProps) => {
const currentPage = metadata?.pagination.page ?? "1";
const totalPages = metadata?.pagination.pages;
const totalEntries = metadata?.pagination.count;
return { currentPage, totalPages, totalEntries };
};
// FORMAT DATE TIME
export const formatDateTime = (
dateString: Date | string,
timeZone: string = Intl.DateTimeFormat().resolvedOptions().timeZone,
) => {
const dateTimeOptions: Intl.DateTimeFormatOptions = {
// weekday: "short", // abbreviated weekday name (e.g., 'Mon')
month: "short", // abbreviated month name (e.g., 'Oct')
day: "numeric", // numeric day of the month (e.g., '25')
year: "numeric", // numeric year (e.g., '2023')
hour: "numeric", // numeric hour (e.g., '8')
minute: "numeric", // numeric minute (e.g., '30')
hour12: true, // use 12-hour clock (true) or 24-hour clock (false),
timeZone: timeZone, // use the provided timezone
};
const dateDayOptions: Intl.DateTimeFormatOptions = {
weekday: "short", // abbreviated weekday name (e.g., 'Mon')
year: "numeric", // numeric year (e.g., '2023')
month: "2-digit", // abbreviated month name (e.g., 'Oct')
day: "2-digit", // numeric day of the month (e.g., '25')
timeZone: timeZone, // use the provided timezone
};
const dateOptions: Intl.DateTimeFormatOptions = {
month: "short", // abbreviated month name (e.g., 'Oct')
year: "numeric", // numeric year (e.g., '2023')
day: "numeric", // numeric day of the month (e.g., '25')
timeZone: timeZone, // use the provided timezone
};
const timeOptions: Intl.DateTimeFormatOptions = {
hour: "numeric", // numeric hour (e.g., '8')
minute: "numeric", // numeric minute (e.g., '30')
hour12: true, // use 12-hour clock (true) or 24-hour clock (false)
timeZone: timeZone, // use the provided timezone
};
const formattedDateTime: string = new Date(dateString).toLocaleString(
"en-US",
dateTimeOptions,
);
const formattedDateDay: string = new Date(dateString).toLocaleString(
"en-US",
dateDayOptions,
);
const formattedDate: string = new Date(dateString).toLocaleString(
"en-US",
dateOptions,
);
const formattedTime: string = new Date(dateString).toLocaleString(
"en-US",
timeOptions,
);
return {
dateTime: formattedDateTime,
dateDay: formattedDateDay,
dateOnly: formattedDate,
timeOnly: formattedTime,
};
};
export function encryptKey(passkey: string) {
return btoa(passkey);
}
export function decryptKey(passkey: string) {
return atob(passkey);
}
+379
View File
@@ -28,6 +28,7 @@
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"framer-motion": "~11.1.1",
"intl-messageformat": "^10.5.0",
@@ -6308,6 +6309,384 @@
"node": ">=6"
}
},
"node_modules/cmdk": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
"integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/cmdk/node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-context": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.4",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-escape-keydown": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-portal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-presence": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-callback-ref": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/cmdk/node_modules/react-remove-scroll": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.3",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.0",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/code-block-writer": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz",
+1
View File
@@ -20,6 +20,7 @@
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"framer-motion": "~11.1.1",
"intl-messageformat": "^10.5.0",
+7
View File
@@ -30,3 +30,10 @@
--chart-5: 340 75% 55%;
}
}
@layer utilities {
/* Hide scrollbar */
.no-scrollbar {
scrollbar-width: none;
}
}
+72
View File
@@ -19,6 +19,36 @@ module.exports = {
},
extend: {
colors: {
prowler: {
blue: {
midnight: "#030921",
pale: "#f3fcff",
smoky: "#7b8390",
},
grey: {
medium: "#353a4d",
light: "#868994",
},
green: {
DEFAULT: "#9FD655",
medium: "#09BF3D",
},
theme: {
green: "#6af400",
purple: "#5001d0",
coral: "#ff5356",
orange: "#f69000",
yellow: "#ffdf16",
},
dark: {
title: "#E2E8F0",
text: "#94a3b8" /* primary default for dark mode */,
},
light: {
title: "#1e293bff",
text: "#64748b" /* primary default for light mode */,
},
},
system: {
success: {
DEFAULT: "#09BF3D",
@@ -51,11 +81,53 @@ module.exports = {
low: "#fcd34d",
},
},
danger: "#E11D48",
},
fontFamily: {
sans: ["var(--font-sans)"],
mono: ["var(--font-geist-mono)"],
},
boxShadow: {
"box-light":
"-16px -16px 40px rgba(255, 255, 255, 0.8), 16px 4px 64px rgba(18, 61, 101, 0.3), inset -8px -6px 80px rgba(255, 255, 255, 0.18)",
"button-curved-default":
"-4px -2px 16px rgba(255, 255, 255, 0.7), 4px 2px 16px rgba(136, 165, 191, 0.38)",
"button-curved-pressed":
"inset -4px -4px 16px rgba(255, 255, 255, 0.8), inset 4px 4px 12px rgba(136, 165, 191, 0.4)",
"button-flat-nopressed":
"-4px -2px 16px #FFFFFF, 4px 2px 16px rgba(136, 165, 191, 0.48)",
"button-flat-pressed":
"inset -3px -3px 7px #FFFFFF, inset 3px 3px 7px rgba(136, 165, 191, 0.48)",
"box-down-light":
"inset -3px -3px 7px #FFFFFF, inset 2px 2px 5px rgba(136, 165, 191, 0.38)",
"box-up":
"-4px -2px 16px #FFFFFF, 4px 2px 16px rgba(136, 165, 191, 0.54)",
"box-dark":
"-4px -2px 16px rgba(195, 200, 205, 0.09), 4px 4px 18px rgba(0, 0, 0, 0.5)",
"box-dark-out": "inset 2px 2px 2px rgba(26, 32, 38, 0.4)",
"buttons-box-dark":
"-5px -6px 16px rgba(195, 200, 205, 0.04), 22px 22px 60px rgba(0, 0, 0, 0.5)",
"button-curved-default-dark":
"-4px -4px 16px rgba(195, 200, 205, 0.06), 4px 4px 18px rgba(0, 0, 0, 0.6)",
"button-curved-pressed-dark":
"-4px -2px 16px rgba(195, 200, 205, 0.07), 4px 4px 18px rgba(0, 0, 0, 0.44)",
"sky-light":
"-16px 20px 40px rgba(215, 215, 215, 0.3), -2px 2px 24px rgba(22, 28, 47, 0.3), -16px 28px 120px rgba(0, 0, 0, 0.1)",
"midnight-dark":
"-16px 20px 40px rgba(3, 9, 33, 0.3), -2px 2px 24px rgba(3, 9, 33, 0.6), -16px 28px 120px rgba(3, 9, 33, 0.1)",
switcher:
"0px -6px 24px #FFFFFF, 0px 7px 16px rgba(104, 132, 157, 0.5)",
up: "0.3rem 0.3rem 0.6rem #c8d0e7, -0.2rem -0.2rem 0.5rem #fff",
down: "inset 0.2rem 0.2rem 0.5rem #c8d0e7, inset -0.2rem -0.2rem 0.5rem #fff",
},
animation: {
"fade-in": "fade-in 200ms ease-out 0s 1 normal forwards running",
"fade-out": "fade-out 200ms ease-in 0s 1 normal forwards running",
expand: "expand 400ms linear 0s 1 normal forwards running",
"slide-in": "slide-in 400ms linear 0s 1 normal forwards running",
"slide-out": "slide-out 400ms linear 0s 1 normal forwards running",
collapse: "collapse 400ms linear 0s 1 normal forwards running",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
+19 -2
View File
@@ -9,6 +9,23 @@ export type IconProps = {
style?: React.CSSProperties;
};
export type NextUIVariants =
| "solid"
| "faded"
| "bordered"
| "light"
| "flat"
| "ghost"
| "shadow";
export type NextUIColors =
| "primary"
| "secondary"
| "success"
| "warning"
| "danger"
| "default";
export interface SearchParamsProps {
[key: string]: string | string[] | undefined;
}
@@ -16,8 +33,8 @@ export interface ProviderProps {
id: string;
type: "providers";
attributes: {
provider: "aws" | "azure" | "gcp";
provider_id: string;
provider: "aws" | "azure" | "gcp" | "kubernetes";
uid: string;
alias: string;
status: "completed" | "pending" | "cancelled";
resources: number;
+20
View File
@@ -0,0 +1,20 @@
export interface FilterOption {
key: string;
labelCheckboxGroup: string;
values: string[];
}
export interface CustomDropdownFilterProps {
filter: FilterOption;
onFilterChange: (key: string, values: string[]) => void;
}
export interface FilterControlsProps {
search?: boolean;
providers?: boolean;
date?: boolean;
regions?: boolean;
accounts?: boolean;
mutedFindings?: boolean;
customFilters?: FilterOption[];
}
+6
View File
@@ -1,5 +1,11 @@
import { z } from "zod";
export const addProviderFormSchema = z.object({
providerType: z.string(),
providerAlias: z.string(),
providerId: z.string(),
});
export const editProviderFormSchema = (currentAlias: string) =>
z.object({
alias: z
+1
View File
@@ -1,3 +1,4 @@
export * from "./authFormSchema";
export * from "./components";
export * from "./filters";
export * from "./formSchemas";