mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
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:
+56
-36
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
+5
-4
@@ -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"
|
||||
/>
|
||||
);
|
||||
+7
-10
@@ -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);
|
||||
@@ -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
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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" />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
export * from "./AddForm";
|
||||
export * from "./DeleteForm";
|
||||
export * from "./EditForm";
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
));
|
||||
@@ -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);
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui";
|
||||
} from "@/components/ui/table";
|
||||
import { MetaDataProps } from "@/types";
|
||||
|
||||
import { DataTablePagination } from "./DataTablePagination";
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
export * from "./custom";
|
||||
export * from "./seed";
|
||||
export * from "./utils";
|
||||
|
||||
+1
-104
@@ -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);
|
||||
}
|
||||
|
||||
Generated
+379
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -30,3 +30,10 @@
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar */
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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,3 +1,4 @@
|
||||
export * from "./authFormSchema";
|
||||
export * from "./components";
|
||||
export * from "./filters";
|
||||
export * from "./formSchemas";
|
||||
|
||||
Reference in New Issue
Block a user