mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-31 07:44:21 +00:00
Merge remote-tracking branch 'origin/master' into refactor/graph-components-kebab-case
This commit is contained in:
146 files changed
+6936
-8745
No files matched your search
+2
-1
@@ -33,4 +33,5 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
next-env.d.ts
|
||||
playwright/.auth
|
||||
@@ -16,6 +16,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- API key management in user profile [(#8308)](https://github.com/prowler-cloud/prowler/pull/8308)
|
||||
- Refresh access token error handling [(#8864)](https://github.com/prowler-cloud/prowler/pull/8864)
|
||||
- Support Common Cloud Controls for AWS, Azure and GCP [(#8000)](https://github.com/prowler-cloud/prowler/pull/8000)
|
||||
- New M365 credentials certificate authentication method [(#8929)](https://github.com/prowler-cloud/prowler/pull/8929)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -36,6 +37,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- ThreatScore for each pillar in Prowler ThreatScore specific view [(#8582)](https://github.com/prowler-cloud/prowler/pull/8582)
|
||||
- Remove maxTokens model param for GPT-5 models [(#8843)](https://github.com/prowler-cloud/prowler/pull/8843)
|
||||
- MITRE ATTACK compliance view now shows all requirements in charts [(#8886)](https://github.com/prowler-cloud/prowler/pull/8886)
|
||||
- Mutelist menu item now doesn't blink [(#8932)](https://github.com/prowler-cloud/prowler/pull/8932)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SelectViaGCP,
|
||||
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
|
||||
import { getProviderFormType } from "@/lib/provider-helpers";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
@@ -28,6 +29,7 @@ export default async function AddCredentialsPage({ searchParams }: Props) {
|
||||
if (providerType === "gcp") return <SelectViaGCP initialVia={via} />;
|
||||
if (providerType === "github")
|
||||
return <SelectViaGitHub initialVia={via} />;
|
||||
if (providerType === "m365") return <SelectViaM365 initialVia={via} />;
|
||||
return null;
|
||||
|
||||
case "credentials":
|
||||
|
||||
@@ -28,31 +28,36 @@ export default async function Providers({
|
||||
<ContentLayout title="Cloud Providers" icon="lucide:cloud-cog">
|
||||
<FilterControls search customFilters={filterProviders || []} />
|
||||
<Spacer y={8} />
|
||||
<Suspense
|
||||
key={searchParamsKey}
|
||||
fallback={
|
||||
<>
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<ManageGroupsButton />
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12">
|
||||
<SkeletonTableProviders />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ProvidersContent searchParams={resolvedSearchParams} />
|
||||
<ProvidersActions />
|
||||
<Spacer y={8} />
|
||||
<Suspense key={searchParamsKey} fallback={<ProvidersTableFallback />}>
|
||||
<ProvidersTable searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const ProvidersContent = async ({
|
||||
const ProvidersActions = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<ManageGroupsButton />
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProvidersTableFallback = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12">
|
||||
<SkeletonTableProviders />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProvidersTable = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
@@ -97,13 +102,6 @@ const ProvidersContent = async ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-4 md:justify-end">
|
||||
<ManageGroupsButton />
|
||||
<MutedFindingsConfigButton />
|
||||
<AddProviderButton />
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-12">
|
||||
<DataTable
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/shadcn",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface UpdateCredentialsInfoProps {
|
||||
@@ -24,6 +25,9 @@ export const CredentialsUpdateInfo = ({
|
||||
if (providerType === "github") {
|
||||
return <SelectViaGitHub initialVia={initialVia} />;
|
||||
}
|
||||
if (providerType === "m365") {
|
||||
return <SelectViaM365 initialVia={initialVia} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { SettingsIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
|
||||
import { useUIStore } from "@/store/ui/store";
|
||||
@@ -8,13 +10,38 @@ import { useUIStore } from "@/store/ui/store";
|
||||
import { MutedFindingsConfigForm } from "./forms";
|
||||
|
||||
export const MutedFindingsConfigButton = () => {
|
||||
const pathname = usePathname();
|
||||
const {
|
||||
isMutelistModalOpen,
|
||||
openMutelistModal,
|
||||
closeMutelistModal,
|
||||
hasProviders,
|
||||
shouldAutoOpenMutelist,
|
||||
resetMutelistModalRequest,
|
||||
} = useUIStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAutoOpenMutelist) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname !== "/providers") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasProviders) {
|
||||
openMutelistModal();
|
||||
}
|
||||
|
||||
resetMutelistModalRequest();
|
||||
}, [
|
||||
hasProviders,
|
||||
openMutelistModal,
|
||||
pathname,
|
||||
resetMutelistModalRequest,
|
||||
shouldAutoOpenMutelist,
|
||||
]);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
if (hasProviders) {
|
||||
openMutelistModal();
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
GCPDefaultCredentials,
|
||||
GCPServiceAccountKey,
|
||||
KubernetesCredentials,
|
||||
M365Credentials,
|
||||
M365CertificateCredentials,
|
||||
M365ClientSecretCredentials,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
@@ -26,10 +27,13 @@ import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credenti
|
||||
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form";
|
||||
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
|
||||
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
|
||||
import {
|
||||
M365CertificateCredentialsForm,
|
||||
M365ClientSecretCredentialsForm,
|
||||
} from "./select-credentials-type/m365";
|
||||
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
|
||||
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
|
||||
|
||||
type BaseCredentialsFormProps = {
|
||||
providerType: ProviderType;
|
||||
@@ -103,11 +107,22 @@ export const BaseCredentialsForm = ({
|
||||
control={form.control as unknown as Control<AzureCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" && (
|
||||
<M365CredentialsForm
|
||||
control={form.control as unknown as Control<M365Credentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" &&
|
||||
searchParamsObj.get("via") === "app_client_secret" && (
|
||||
<M365ClientSecretCredentialsForm
|
||||
control={
|
||||
form.control as unknown as Control<M365ClientSecretCredentials>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" &&
|
||||
searchParamsObj.get("via") === "app_certificate" && (
|
||||
<M365CertificateCredentialsForm
|
||||
control={
|
||||
form.control as unknown as Control<M365CertificateCredentials>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" &&
|
||||
searchParamsObj.get("via") === "service-account" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
@@ -159,6 +174,15 @@ export const BaseCredentialsForm = ({
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
onPress={(e) => {
|
||||
const formElement = e.target as HTMLElement;
|
||||
const form = formElement.closest("form");
|
||||
if (form) {
|
||||
form.dispatchEvent(
|
||||
new Event("submit", { bubbles: true, cancelable: true }),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>{submitButtonText}</span>}
|
||||
</CustomButton>
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { M365CertificateCredentialsForm } from "./m365-certificate-credentials-form";
|
||||
export { M365ClientSecretCredentialsForm } from "./m365-client-secret-credentials-form";
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365CertificateCredentials } from "@/types";
|
||||
|
||||
export const M365CertificateCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365CertificateCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
App Certificate Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide your Microsoft 365 application credentials with
|
||||
certificate authentication.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomTextarea
|
||||
control={control}
|
||||
name="certificate_content"
|
||||
label="Certificate Content"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the base64 encoded certificate content"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.certificate_content}
|
||||
minRows={4}
|
||||
/>
|
||||
<p className="text-default-500 text-sm">
|
||||
The certificate content must be base64 encoded from an unsigned
|
||||
certificate. For detailed instructions on how to generate and encode
|
||||
your certificate, please refer to the{" "}
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/user-guide/providers/microsoft365/authentication#generate-the-certificate"
|
||||
size="sm"
|
||||
>
|
||||
certificate generation guide
|
||||
</CustomLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { M365ClientSecretCredentials } from "@/types";
|
||||
|
||||
export const M365ClientSecretCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365ClientSecretCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
App Client Secret Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide your Microsoft 365 application credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_secret"
|
||||
type="password"
|
||||
label="Client Secret"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client Secret"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_secret}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
M365CertificateCredentialsForm,
|
||||
M365ClientSecretCredentialsForm,
|
||||
} from "./credentials-type";
|
||||
export { SelectViaM365 } from "./select-via-m365";
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { RadioGroup } from "@heroui/radio";
|
||||
import React from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { CustomRadio } from "@/components/ui/custom";
|
||||
import { FormMessage } from "@/components/ui/form";
|
||||
|
||||
type RadioGroupM365ViaCredentialsFormProps = {
|
||||
control: Control<any>;
|
||||
isInvalid: boolean;
|
||||
errorMessage?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const RadioGroupM365ViaCredentialsTypeForm = ({
|
||||
control,
|
||||
isInvalid,
|
||||
errorMessage,
|
||||
onChange,
|
||||
}: RadioGroupM365ViaCredentialsFormProps) => {
|
||||
return (
|
||||
<Controller
|
||||
name="m365CredentialsType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<RadioGroup
|
||||
className="flex flex-wrap"
|
||||
isInvalid={isInvalid}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-default-500 text-sm">
|
||||
Select Authentication Method
|
||||
</span>
|
||||
<CustomRadio
|
||||
description="Connect using Application Client Secret"
|
||||
value="app_client_secret"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">App Client Secret Credentials</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio
|
||||
description="Connect using Application Certificate"
|
||||
value="app_certificate"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">App Certificate Credentials</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{errorMessage && (
|
||||
<FormMessage className="text-system-error dark:text-system-error">
|
||||
{errorMessage}
|
||||
</FormMessage>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
import { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form";
|
||||
|
||||
interface SelectViaM365Props {
|
||||
initialVia?: string;
|
||||
}
|
||||
|
||||
export const SelectViaM365 = ({ initialVia }: SelectViaM365Props) => {
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
m365CredentialsType: initialVia || "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSelectionChange = (value: string) => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("via", value);
|
||||
router.push(url.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<RadioGroupM365ViaCredentialsTypeForm
|
||||
control={form.control}
|
||||
isInvalid={!!form.formState.errors.m365CredentialsType}
|
||||
errorMessage={form.formState.errors.m365CredentialsType?.message}
|
||||
onChange={handleSelectionChange}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./azure-credentials-form";
|
||||
export * from "./github-credentials-form";
|
||||
export * from "./k8s-credentials-form";
|
||||
export * from "./m365-credentials-form";
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365Credentials } from "@/types";
|
||||
|
||||
export const M365CredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365Credentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Connect via Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide the information for your Microsoft 365 credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_secret"
|
||||
type="password"
|
||||
label="Client Secret"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client Secret"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_secret}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<p className="text-default-500 text-sm">
|
||||
{" "}
|
||||
User and password authentication is being deprecated due to
|
||||
Microsoft's on-going MFA enforcement across all tenants (see{" "}
|
||||
<CustomLink
|
||||
href="https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/"
|
||||
size="sm"
|
||||
>
|
||||
Microsoft docs
|
||||
</CustomLink>
|
||||
).
|
||||
</p>
|
||||
|
||||
<div className="border-system-warning bg-system-warning-medium dark:text-default-300 flex items-center rounded-lg border p-2 text-sm">
|
||||
<InfoIcon className="mr-2 inline h-4 w-4 shrink-0" />
|
||||
<p className="text-xs font-extrabold">
|
||||
By October 2025, MFA will be mandatory.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-default-500 text-sm">
|
||||
Due to that change, you must only{" "}
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/#step-3-configure-your-m365-account"
|
||||
size="sm"
|
||||
>
|
||||
use application authentication
|
||||
</CustomLink>{" "}
|
||||
to maintain all Prowler M365 scan capabilities.
|
||||
</p>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="user"
|
||||
type="text"
|
||||
label="User"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the User"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.user}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Password"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.password}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
# shadcn Components
|
||||
|
||||
This directory contains all shadcn/ui based components for the Prowler application.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
shadcn/
|
||||
├── card.tsx # shadcn Card component
|
||||
├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn
|
||||
│ ├── resource-stats-card.tsx
|
||||
│ ├── resource-stats-card.example.tsx
|
||||
│ └── index.ts
|
||||
├── index.ts # Barrel exports
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
All shadcn components can be imported from `@/components/shadcn`:
|
||||
|
||||
```tsx
|
||||
import { Card, CardHeader, CardContent } from "@/components/shadcn";
|
||||
import { ResourceStatsCard } from "@/components/shadcn";
|
||||
```
|
||||
|
||||
## Adding New shadcn Components
|
||||
|
||||
When adding new shadcn components using the CLI:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add [component-name]
|
||||
```
|
||||
|
||||
The component will be automatically added to this directory due to the configuration in `components.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"aliases": {
|
||||
"ui": "@/components/shadcn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component Guidelines
|
||||
|
||||
1. **shadcn base components** - Use as-is from shadcn/ui (e.g., `card.tsx`)
|
||||
2. **Custom components built on shadcn** - Create in subdirectories (e.g., `resource-stats-card/`)
|
||||
3. **CVA variants** - Use Class Variance Authority for type-safe variants
|
||||
4. **Theme support** - Include `dark:` classes for dark/light theme compatibility
|
||||
5. **TypeScript** - Always export types and use proper typing
|
||||
|
||||
## Resources
|
||||
|
||||
- [shadcn/ui Documentation](https://ui.shadcn.com)
|
||||
- [CVA Documentation](https://cva.style/docs)
|
||||
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./card";
|
||||
export {
|
||||
ResourceStatsCard,
|
||||
ResourceStatsCardContainer,
|
||||
type ResourceStatsCardContainerProps,
|
||||
ResourceStatsCardContent,
|
||||
type ResourceStatsCardContentProps,
|
||||
ResourceStatsCardDivider,
|
||||
type ResourceStatsCardDividerProps,
|
||||
ResourceStatsCardHeader,
|
||||
type ResourceStatsCardHeaderProps,
|
||||
type ResourceStatsCardProps,
|
||||
type StatItem,
|
||||
} from "./resource-stats-card";
|
||||
@@ -0,0 +1,13 @@
|
||||
export type { ResourceStatsCardProps } from "./resource-stats-card";
|
||||
export { ResourceStatsCard } from "./resource-stats-card";
|
||||
export type { ResourceStatsCardContainerProps } from "./resource-stats-card-container";
|
||||
export { ResourceStatsCardContainer } from "./resource-stats-card-container";
|
||||
export type {
|
||||
ResourceStatsCardContentProps,
|
||||
StatItem,
|
||||
} from "./resource-stats-card-content";
|
||||
export { ResourceStatsCardContent } from "./resource-stats-card-content";
|
||||
export type { ResourceStatsCardDividerProps } from "./resource-stats-card-divider";
|
||||
export { ResourceStatsCardDivider } from "./resource-stats-card-divider";
|
||||
export type { ResourceStatsCardHeaderProps } from "./resource-stats-card-header";
|
||||
export { ResourceStatsCardHeader } from "./resource-stats-card-header";
|
||||
@@ -0,0 +1,55 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const containerVariants = cva(
|
||||
[
|
||||
"flex",
|
||||
"rounded-[12px]",
|
||||
"border",
|
||||
"backdrop-blur-[46px]",
|
||||
"border-[rgba(38,38,38,0.70)]",
|
||||
"bg-[rgba(23,23,23,0.50)]",
|
||||
"dark:border-[rgba(38,38,38,0.70)]",
|
||||
"dark:bg-[rgba(23,23,23,0.50)]",
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
padding: {
|
||||
sm: "px-3 py-2",
|
||||
md: "px-[19px] py-[9px]",
|
||||
lg: "px-6 py-3",
|
||||
none: "p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
padding: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ResourceStatsCardContainerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof containerVariants> {
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCardContainer = ({
|
||||
className,
|
||||
children,
|
||||
padding,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(containerVariants({ padding }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCardContainer.displayName = "ResourceStatsCardContainer";
|
||||
@@ -0,0 +1,204 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface StatItem {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const CardVariant = {
|
||||
default: "default",
|
||||
fail: "fail",
|
||||
pass: "pass",
|
||||
warning: "warning",
|
||||
info: "info",
|
||||
} as const;
|
||||
|
||||
export type CardVariant = (typeof CardVariant)[keyof typeof CardVariant];
|
||||
|
||||
const variantColors = {
|
||||
default: "#868994",
|
||||
fail: "#f54280",
|
||||
pass: "#4ade80",
|
||||
warning: "#fbbf24",
|
||||
info: "#60a5fa",
|
||||
} as const;
|
||||
|
||||
type BadgeVariant = keyof typeof variantColors;
|
||||
|
||||
const badgeVariants = cva(
|
||||
["flex", "items-center", "justify-center", "gap-0.5", "rounded-full"],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
[CardVariant.default]: "bg-[#535359]",
|
||||
[CardVariant.fail]: "bg-[#432232]",
|
||||
[CardVariant.pass]: "bg-[#204237]",
|
||||
[CardVariant.warning]: "bg-[#3d3520]",
|
||||
[CardVariant.info]: "bg-[#1e3a5f]",
|
||||
},
|
||||
size: {
|
||||
sm: "px-1 text-xs",
|
||||
md: "px-1.5 text-sm",
|
||||
lg: "px-2 text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: CardVariant.fail,
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const badgeIconVariants = cva("", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-2.5 w-2.5",
|
||||
md: "h-3 w-3",
|
||||
lg: "h-4 w-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
const labelTextVariants = cva(
|
||||
"leading-6 font-semibold text-zinc-300 dark:text-zinc-300",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const statIconVariants = cva("text-zinc-300 dark:text-zinc-300", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-2.5 w-2.5",
|
||||
md: "h-3 w-3",
|
||||
lg: "h-3.5 w-3.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
const statLabelVariants = cva(
|
||||
"leading-5 font-medium text-zinc-300 dark:text-zinc-300",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-xs",
|
||||
md: "text-sm",
|
||||
lg: "text-base",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ResourceStatsCardContentProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
badge: {
|
||||
icon: LucideIcon;
|
||||
count: number | string;
|
||||
variant?: CardVariant;
|
||||
};
|
||||
label: string;
|
||||
stats?: StatItem[];
|
||||
accentColor?: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCardContent = ({
|
||||
badge,
|
||||
label,
|
||||
stats = [],
|
||||
accentColor,
|
||||
size = "md",
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardContentProps) => {
|
||||
const BadgeIcon = badge.icon;
|
||||
const badgeVariant: BadgeVariant = badge.variant || "fail";
|
||||
|
||||
// Determine accent line color
|
||||
const lineColor = accentColor || variantColors[badgeVariant] || "#d4d4d8";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col gap-[5px]", className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Badge and Label Row */}
|
||||
<div className="flex w-full items-center gap-1">
|
||||
{/* Badge */}
|
||||
<div className={cn(badgeVariants({ variant: badgeVariant, size }))}>
|
||||
<BadgeIcon
|
||||
className={badgeIconVariants({ size })}
|
||||
strokeWidth={2.5}
|
||||
style={{ color: variantColors[badgeVariant] }}
|
||||
/>
|
||||
<span
|
||||
className="leading-6 font-bold"
|
||||
style={{ color: variantColors[badgeVariant] }}
|
||||
>
|
||||
{badge.count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<span className={labelTextVariants({ size })}>{label}</span>
|
||||
</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
{stats.length > 0 && (
|
||||
<div className="flex w-full items-stretch gap-0">
|
||||
{/* Vertical Accent Line */}
|
||||
<div className="flex items-stretch px-3 py-1">
|
||||
<div
|
||||
className="w-px rounded-full"
|
||||
style={{ backgroundColor: lineColor }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats List */}
|
||||
<div className="flex flex-1 flex-col gap-0.5">
|
||||
{stats.map((stat, index) => {
|
||||
const StatIcon = stat.icon;
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-1">
|
||||
<StatIcon
|
||||
className={statIconVariants({ size })}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className={statLabelVariants({ size })}>
|
||||
{stat.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCardContent.displayName = "ResourceStatsCardContent";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const dividerVariants = cva("flex items-center justify-center", {
|
||||
variants: {
|
||||
spacing: {
|
||||
sm: "px-2",
|
||||
md: "px-[23px]",
|
||||
lg: "px-8",
|
||||
},
|
||||
orientation: {
|
||||
vertical: "h-full",
|
||||
horizontal: "w-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
spacing: "md",
|
||||
orientation: "vertical",
|
||||
},
|
||||
});
|
||||
|
||||
const lineVariants = cva("bg-[rgba(39,39,42,1)]", {
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "h-full w-px",
|
||||
horizontal: "w-full h-px",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
});
|
||||
|
||||
export interface ResourceStatsCardDividerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof dividerVariants> {
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCardDivider = ({
|
||||
className,
|
||||
spacing,
|
||||
orientation,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardDividerProps) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(dividerVariants({ spacing, orientation }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className={lineVariants({ orientation })} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCardDivider.displayName = "ResourceStatsCardDivider";
|
||||
@@ -0,0 +1,103 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const headerVariants = cva("flex w-full items-center gap-1", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "",
|
||||
md: "",
|
||||
lg: "",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
const iconVariants = cva("text-zinc-300 dark:text-zinc-300", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-3.5 w-3.5",
|
||||
md: "h-4 w-4",
|
||||
lg: "h-5 w-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
const titleVariants = cva(
|
||||
"leading-7 font-semibold text-zinc-300 dark:text-zinc-300",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-sm",
|
||||
md: "text-base",
|
||||
lg: "text-lg",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const countVariants = cva(
|
||||
"leading-4 font-normal text-zinc-300 dark:text-zinc-300",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-[9px]",
|
||||
md: "text-[10px]",
|
||||
lg: "text-xs",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ResourceStatsCardHeaderProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof headerVariants> {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
resourceCount?: number | string;
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCardHeader = ({
|
||||
icon: Icon,
|
||||
title,
|
||||
resourceCount,
|
||||
size = "md",
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(headerVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<Icon className={iconVariants({ size })} strokeWidth={2} />
|
||||
<span className={titleVariants({ size })}>{title}</span>
|
||||
</div>
|
||||
{resourceCount !== undefined && (
|
||||
<span className={countVariants({ size })}>
|
||||
{typeof resourceCount === "number"
|
||||
? `${resourceCount} Resources`
|
||||
: resourceCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCardHeader.displayName = "ResourceStatsCardHeader";
|
||||
@@ -0,0 +1,164 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { ResourceStatsCardContainer } from "./resource-stats-card-container";
|
||||
import type { StatItem } from "./resource-stats-card-content";
|
||||
import {
|
||||
CardVariant,
|
||||
ResourceStatsCardContent,
|
||||
} from "./resource-stats-card-content";
|
||||
import { ResourceStatsCardHeader } from "./resource-stats-card-header";
|
||||
|
||||
export type { StatItem };
|
||||
|
||||
// Todo: when the design system is ready, we must use the colors from the design system (semantic colors)
|
||||
// Variant styles using CVA for type safety and consistency
|
||||
// Colors are exact HEX values from Figma design system
|
||||
const cardVariants = cva("", {
|
||||
variants: {
|
||||
variant: {
|
||||
[CardVariant.default]: "",
|
||||
// Fail variant - rgba(67,34,50) from Figma
|
||||
[CardVariant.fail]:
|
||||
"border-[rgba(67,34,50,0.5)] bg-[rgba(67,34,50,0.2)] dark:border-[rgba(67,34,50,0.7)] dark:bg-[rgba(67,34,50,0.3)]",
|
||||
// Pass variant - rgba(32,66,55) from Figma
|
||||
[CardVariant.pass]:
|
||||
"border-[rgba(32,66,55,0.5)] bg-[rgba(32,66,55,0.2)] dark:border-[rgba(32,66,55,0.7)] dark:bg-[rgba(32,66,55,0.3)]",
|
||||
// Warning variant - rgba(61,53,32) from Figma
|
||||
[CardVariant.warning]:
|
||||
"border-[rgba(61,53,32,0.5)] bg-[rgba(61,53,32,0.2)] dark:border-[rgba(61,53,32,0.7)] dark:bg-[rgba(61,53,32,0.3)]",
|
||||
// Info variant - rgba(30,58,95) from Figma
|
||||
[CardVariant.info]:
|
||||
"border-[rgba(30,58,95,0.5)] bg-[rgba(30,58,95,0.2)] dark:border-[rgba(30,58,95,0.7)] dark:bg-[rgba(30,58,95,0.3)]",
|
||||
},
|
||||
size: {
|
||||
sm: "px-2 py-1.5 gap-1",
|
||||
md: "px-3 py-2 gap-2",
|
||||
lg: "px-4 py-3 gap-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: CardVariant.default,
|
||||
size: "md",
|
||||
},
|
||||
});
|
||||
|
||||
export interface ResourceStatsCardProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
||||
VariantProps<typeof cardVariants> {
|
||||
// Optional header (icon + title + resource count)
|
||||
header?: {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
resourceCount?: number | string;
|
||||
};
|
||||
|
||||
// Empty state message (when there's no data to display)
|
||||
emptyState?: {
|
||||
message: string;
|
||||
};
|
||||
|
||||
// Main badge (top section) - optional when using empty state
|
||||
badge?: {
|
||||
icon: LucideIcon;
|
||||
count: number | string;
|
||||
variant?: CardVariant;
|
||||
};
|
||||
|
||||
// Main label - optional when using empty state
|
||||
label?: string;
|
||||
|
||||
// Vertical accent line color (optional, auto-determined from variant)
|
||||
accentColor?: string;
|
||||
|
||||
// Sub-statistics array (flexible items)
|
||||
stats?: StatItem[];
|
||||
|
||||
// Render without container (no border, background, padding) - useful for composing multiple cards in a custom container
|
||||
containerless?: boolean;
|
||||
|
||||
// Ref for the root element
|
||||
ref?: React.Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const ResourceStatsCard = ({
|
||||
header,
|
||||
emptyState,
|
||||
badge,
|
||||
label,
|
||||
accentColor,
|
||||
stats = [],
|
||||
variant = CardVariant.default,
|
||||
size = "md",
|
||||
containerless = false,
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: ResourceStatsCardProps) => {
|
||||
// Resolve size to ensure it's not null (CVA can return null but we need a defined value)
|
||||
const resolvedSize = size || "md";
|
||||
|
||||
// If containerless, render without outer wrapper
|
||||
if (containerless) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col gap-[5px]", className)}
|
||||
{...props}
|
||||
>
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<p className="text-center text-sm leading-5 font-medium text-zinc-300 dark:text-zinc-300">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
badge &&
|
||||
label && (
|
||||
<ResourceStatsCardContent
|
||||
badge={badge}
|
||||
label={label}
|
||||
stats={stats}
|
||||
accentColor={accentColor}
|
||||
size={resolvedSize}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, render with container
|
||||
return (
|
||||
<ResourceStatsCardContainer
|
||||
ref={ref}
|
||||
className={cn(cardVariants({ variant, size }), "flex-col", className)}
|
||||
{...props}
|
||||
>
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<p className="text-center text-sm leading-5 font-medium text-zinc-300 dark:text-zinc-300">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
badge &&
|
||||
label && (
|
||||
<ResourceStatsCardContent
|
||||
badge={badge}
|
||||
label={label}
|
||||
stats={stats}
|
||||
accentColor={accentColor}
|
||||
size={resolvedSize}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ResourceStatsCardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
ResourceStatsCard.displayName = "ResourceStatsCard";
|
||||
@@ -69,11 +69,13 @@ const hideMenuItems = (menuGroups: GroupProps[], labelsToHide: string[]) => {
|
||||
export const Menu = ({ isOpen }: { isOpen: boolean }) => {
|
||||
const pathname = usePathname();
|
||||
const { permissions } = useAuth();
|
||||
const { hasProviders, openMutelistModal } = useUIStore();
|
||||
const { hasProviders, openMutelistModal, requestMutelistModalOpen } =
|
||||
useUIStore();
|
||||
const menuList = getMenuList({
|
||||
pathname,
|
||||
hasProviders,
|
||||
openMutelistModal,
|
||||
requestMutelistModalOpen,
|
||||
});
|
||||
|
||||
const labelsToHide = MENU_HIDE_RULES.filter((rule) =>
|
||||
|
||||
+119
-111
@@ -5,7 +5,7 @@
|
||||
"from": "1.0.59",
|
||||
"to": "1.0.59",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T11:13:12.025Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -13,7 +13,7 @@
|
||||
"from": "2.0.59",
|
||||
"to": "2.0.59",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T11:13:12.025Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -21,15 +21,15 @@
|
||||
"from": "2.8.4",
|
||||
"to": "2.8.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-29T14:26:25.838Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@hookform/resolvers",
|
||||
"from": "3.10.0",
|
||||
"from": "5.2.2",
|
||||
"to": "5.2.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T15:09:44.056Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -37,7 +37,7 @@
|
||||
"from": "0.3.77",
|
||||
"to": "0.3.77",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -45,23 +45,23 @@
|
||||
"from": "0.4.9",
|
||||
"to": "0.4.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/langgraph-supervisor",
|
||||
"from": "0.0.12",
|
||||
"from": "0.0.20",
|
||||
"to": "0.0.20",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/openai",
|
||||
"from": "0.6.9",
|
||||
"from": "0.5.18",
|
||||
"to": "0.5.18",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -69,7 +69,7 @@
|
||||
"from": "15.3.5",
|
||||
"to": "15.3.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -77,7 +77,7 @@
|
||||
"from": "1.1.14",
|
||||
"to": "1.1.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -85,7 +85,7 @@
|
||||
"from": "1.1.14",
|
||||
"to": "1.1.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -93,7 +93,7 @@
|
||||
"from": "2.1.15",
|
||||
"to": "2.1.15",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -101,7 +101,7 @@
|
||||
"from": "1.3.2",
|
||||
"to": "1.3.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -109,7 +109,7 @@
|
||||
"from": "2.1.7",
|
||||
"to": "2.1.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -117,7 +117,7 @@
|
||||
"from": "2.2.5",
|
||||
"to": "2.2.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -125,7 +125,7 @@
|
||||
"from": "1.2.3",
|
||||
"to": "1.2.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -133,7 +133,7 @@
|
||||
"from": "1.2.14",
|
||||
"to": "1.2.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -141,7 +141,7 @@
|
||||
"from": "3.9.4",
|
||||
"to": "3.9.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -149,7 +149,7 @@
|
||||
"from": "3.8.12",
|
||||
"to": "3.8.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -157,7 +157,7 @@
|
||||
"from": "4.1.13",
|
||||
"to": "4.1.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-24T15:04:48.761Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -165,7 +165,7 @@
|
||||
"from": "0.5.16",
|
||||
"to": "0.5.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -173,7 +173,7 @@
|
||||
"from": "8.21.3",
|
||||
"to": "8.21.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -181,15 +181,15 @@
|
||||
"from": "4.0.9",
|
||||
"to": "4.0.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "ai",
|
||||
"from": "4.3.16",
|
||||
"from": "5.0.59",
|
||||
"to": "5.0.59",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T10:03:22.788Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -197,7 +197,7 @@
|
||||
"from": "6.0.2",
|
||||
"to": "6.0.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -205,7 +205,7 @@
|
||||
"from": "0.7.1",
|
||||
"to": "0.7.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -213,7 +213,7 @@
|
||||
"from": "2.1.1",
|
||||
"to": "2.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -221,7 +221,7 @@
|
||||
"from": "7.9.0",
|
||||
"to": "7.9.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-16T10:46:11.221Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -229,7 +229,7 @@
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -237,7 +237,7 @@
|
||||
"from": "11.18.2",
|
||||
"to": "11.18.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -245,7 +245,7 @@
|
||||
"from": "10.7.16",
|
||||
"to": "10.7.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -253,7 +253,7 @@
|
||||
"from": "5.10.0",
|
||||
"to": "5.10.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -261,7 +261,7 @@
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -269,7 +269,7 @@
|
||||
"from": "4.0.0",
|
||||
"to": "4.0.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -277,7 +277,7 @@
|
||||
"from": "0.543.0",
|
||||
"to": "0.543.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -285,15 +285,15 @@
|
||||
"from": "15.0.12",
|
||||
"to": "15.0.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "next",
|
||||
"from": "14.2.32",
|
||||
"from": "15.5.3",
|
||||
"to": "15.5.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -301,7 +301,7 @@
|
||||
"from": "5.0.0-beta.29",
|
||||
"to": "5.0.0-beta.29",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -309,7 +309,7 @@
|
||||
"from": "0.2.1",
|
||||
"to": "0.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -317,23 +317,23 @@
|
||||
"from": "1.4.2",
|
||||
"to": "1.4.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react",
|
||||
"from": "18.3.1",
|
||||
"from": "19.1.1",
|
||||
"to": "19.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react-dom",
|
||||
"from": "18.3.1",
|
||||
"from": "19.1.1",
|
||||
"to": "19.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -341,7 +341,7 @@
|
||||
"from": "7.62.0",
|
||||
"to": "7.62.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -349,7 +349,7 @@
|
||||
"from": "10.1.0",
|
||||
"to": "10.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -357,7 +357,7 @@
|
||||
"from": "2.15.4",
|
||||
"to": "2.15.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -365,7 +365,7 @@
|
||||
"from": "3.13.0",
|
||||
"to": "3.13.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -373,15 +373,7 @@
|
||||
"from": "0.0.1",
|
||||
"to": "0.0.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "shadcn",
|
||||
"from": "3.2.1",
|
||||
"to": "3.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -389,7 +381,7 @@
|
||||
"from": "0.33.5",
|
||||
"to": "0.33.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -397,7 +389,7 @@
|
||||
"from": "3.3.1",
|
||||
"to": "3.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -405,7 +397,7 @@
|
||||
"from": "1.0.7",
|
||||
"to": "1.0.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -413,7 +405,15 @@
|
||||
"from": "3.1.0",
|
||||
"to": "3.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-16T10:46:11.221Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "tw-animate-css",
|
||||
"from": "1.4.0",
|
||||
"to": "1.4.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -421,7 +421,7 @@
|
||||
"from": "11.1.0",
|
||||
"to": "11.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -429,23 +429,23 @@
|
||||
"from": "2.0.2",
|
||||
"to": "2.0.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-16T10:46:11.221Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "zod",
|
||||
"from": "3.25.73",
|
||||
"from": "4.1.11",
|
||||
"to": "4.1.11",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T09:40:25.207Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "zustand",
|
||||
"from": "4.5.7",
|
||||
"from": "5.0.8",
|
||||
"to": "5.0.8",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T09:40:25.207Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -453,15 +453,15 @@
|
||||
"from": "5.2.1",
|
||||
"to": "5.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@playwright/test",
|
||||
"from": "1.53.2",
|
||||
"to": "1.53.2",
|
||||
"from": "1.56.1",
|
||||
"to": "1.56.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -469,7 +469,7 @@
|
||||
"from": "7.4.3",
|
||||
"to": "7.4.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-16T10:46:11.221Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -477,23 +477,23 @@
|
||||
"from": "20.5.7",
|
||||
"to": "20.5.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/react",
|
||||
"from": "18.3.3",
|
||||
"from": "19.1.13",
|
||||
"to": "19.1.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/react-dom",
|
||||
"from": "18.3.0",
|
||||
"from": "19.1.9",
|
||||
"to": "19.1.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -501,7 +501,7 @@
|
||||
"from": "3.1.5",
|
||||
"to": "3.1.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-16T10:46:11.221Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -509,7 +509,7 @@
|
||||
"from": "10.0.0",
|
||||
"to": "10.0.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -517,7 +517,7 @@
|
||||
"from": "7.18.0",
|
||||
"to": "7.18.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -525,7 +525,7 @@
|
||||
"from": "7.18.0",
|
||||
"to": "7.18.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -533,7 +533,7 @@
|
||||
"from": "10.4.19",
|
||||
"to": "10.4.19",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -541,7 +541,7 @@
|
||||
"from": "19.1.0-rc.3",
|
||||
"to": "19.1.0-rc.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -549,15 +549,15 @@
|
||||
"from": "8.57.1",
|
||||
"to": "8.57.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-config-next",
|
||||
"from": "14.2.32",
|
||||
"from": "15.5.3",
|
||||
"to": "15.5.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -565,7 +565,7 @@
|
||||
"from": "10.1.5",
|
||||
"to": "10.1.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -573,7 +573,7 @@
|
||||
"from": "2.32.0",
|
||||
"to": "2.32.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -581,7 +581,7 @@
|
||||
"from": "6.10.2",
|
||||
"to": "6.10.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -589,7 +589,7 @@
|
||||
"from": "11.1.0",
|
||||
"to": "11.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -597,7 +597,7 @@
|
||||
"from": "5.5.1",
|
||||
"to": "5.5.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -605,7 +605,7 @@
|
||||
"from": "7.37.5",
|
||||
"to": "7.37.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -613,7 +613,7 @@
|
||||
"from": "4.6.2",
|
||||
"to": "4.6.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -621,7 +621,7 @@
|
||||
"from": "3.0.1",
|
||||
"to": "3.0.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -629,7 +629,7 @@
|
||||
"from": "12.1.1",
|
||||
"to": "12.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -637,7 +637,7 @@
|
||||
"from": "3.2.0",
|
||||
"to": "3.2.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -645,7 +645,7 @@
|
||||
"from": "9.1.7",
|
||||
"to": "9.1.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -653,7 +653,7 @@
|
||||
"from": "15.5.2",
|
||||
"to": "15.5.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -661,7 +661,7 @@
|
||||
"from": "8.4.38",
|
||||
"to": "8.4.38",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -669,15 +669,23 @@
|
||||
"from": "3.6.2",
|
||||
"to": "3.6.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "prettier-plugin-tailwindcss",
|
||||
"from": "0.6.13",
|
||||
"from": "0.6.14",
|
||||
"to": "0.6.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-24T13:59:11.231Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "shadcn",
|
||||
"from": "3.4.1",
|
||||
"to": "3.4.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -685,15 +693,15 @@
|
||||
"from": "0.1.20",
|
||||
"to": "0.1.20",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "tailwindcss",
|
||||
"from": "3.4.3",
|
||||
"from": "4.1.13",
|
||||
"to": "4.1.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-24T13:59:11.231Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -701,6 +709,6 @@
|
||||
"from": "5.5.4",
|
||||
"to": "5.5.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
}
|
||||
]
|
||||
@@ -40,8 +40,8 @@ export const useCredentialsForm = ({
|
||||
if (providerType === "gcp" && via === "service-account") {
|
||||
return addCredentialsServiceAccountFormSchema(providerType);
|
||||
}
|
||||
// For GitHub, we need to pass the via parameter to determine which fields are required
|
||||
if (providerType === "github") {
|
||||
// For GitHub and M365, we need to pass the via parameter to determine which fields are required
|
||||
if (providerType === "github" || providerType === "m365") {
|
||||
return addCredentialsFormSchema(providerType, via);
|
||||
}
|
||||
return addCredentialsFormSchema(providerType);
|
||||
@@ -99,13 +99,27 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
case "m365":
|
||||
// M365 credentials based on via parameter
|
||||
if (via === "app_client_secret") {
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
}
|
||||
if (via === "app_certificate") {
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
[ProviderCredentialFields.USER]: "",
|
||||
[ProviderCredentialFields.PASSWORD]: "",
|
||||
};
|
||||
case "gcp":
|
||||
return {
|
||||
@@ -146,9 +160,14 @@ export const useCredentialsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const defaultValues = getDefaultValues();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: getDefaultValues(),
|
||||
defaultValues: defaultValues,
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onChange",
|
||||
criteriaMode: "all", // Show all errors for each field
|
||||
});
|
||||
|
||||
const { handleServerResponse } = useFormServerErrors(
|
||||
@@ -169,6 +188,7 @@ export const useCredentialsForm = ({
|
||||
|
||||
// Filter out empty values first, then append all remaining values
|
||||
const filteredValues = filterEmptyValues(values);
|
||||
|
||||
Object.entries(filteredValues).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
@@ -181,9 +201,12 @@ export const useCredentialsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { isSubmitting, errors } = form.formState;
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading: form.formState.isSubmitting,
|
||||
isLoading: isSubmitting,
|
||||
errors,
|
||||
handleSubmit,
|
||||
handleBackStep,
|
||||
searchParamsObj,
|
||||
|
||||
+22
-3
@@ -18,6 +18,7 @@ import {
|
||||
VolumeX,
|
||||
Warehouse,
|
||||
} from "lucide-react";
|
||||
import type { MouseEvent } from "react";
|
||||
|
||||
import { ProwlerShort } from "@/components/icons";
|
||||
import {
|
||||
@@ -39,12 +40,14 @@ interface MenuListOptions {
|
||||
pathname: string;
|
||||
hasProviders?: boolean;
|
||||
openMutelistModal?: () => void;
|
||||
requestMutelistModalOpen?: () => void;
|
||||
}
|
||||
|
||||
export const getMenuList = ({
|
||||
pathname,
|
||||
hasProviders,
|
||||
openMutelistModal,
|
||||
requestMutelistModalOpen,
|
||||
}: MenuListOptions): GroupProps[] => {
|
||||
return [
|
||||
{
|
||||
@@ -164,12 +167,28 @@ export const getMenuList = ({
|
||||
submenus: [
|
||||
{ href: "/providers", label: "Cloud Providers", icon: CloudCog },
|
||||
{
|
||||
// Use trailing slash to prevent both menu items from being active at /providers
|
||||
href: "/providers/",
|
||||
href: "/providers",
|
||||
label: "Mutelist",
|
||||
icon: VolumeX,
|
||||
disabled: hasProviders === false,
|
||||
onClick: openMutelistModal,
|
||||
active: false,
|
||||
onClick: (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (hasProviders === false) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
requestMutelistModalOpen?.();
|
||||
|
||||
if (pathname !== "/providers") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openMutelistModal?.();
|
||||
},
|
||||
},
|
||||
{ href: "/manage-groups", label: "Provider Groups", icon: Group },
|
||||
{ href: "/scans", label: "Scan Jobs", icon: Timer },
|
||||
|
||||
@@ -80,14 +80,21 @@ export const buildAzureSecret = (formData: FormData) => {
|
||||
|
||||
export const buildM365Secret = (formData: FormData) => {
|
||||
const secret = {
|
||||
...buildAzureSecret(formData),
|
||||
[ProviderCredentialFields.USER]: getFormValue(
|
||||
[ProviderCredentialFields.CLIENT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.USER,
|
||||
ProviderCredentialFields.CLIENT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.PASSWORD]: getFormValue(
|
||||
[ProviderCredentialFields.TENANT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.PASSWORD,
|
||||
ProviderCredentialFields.TENANT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.CLIENT_SECRET,
|
||||
),
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.CERTIFICATE_CONTENT,
|
||||
),
|
||||
};
|
||||
return filterEmptyValues(secret);
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ProviderCredentialFields = {
|
||||
TENANT_ID: "tenant_id",
|
||||
USER: "user",
|
||||
PASSWORD: "password",
|
||||
CERTIFICATE_CONTENT: "certificate_content",
|
||||
|
||||
// GCP fields
|
||||
REFRESH_TOKEN: "refresh_token",
|
||||
@@ -70,6 +71,7 @@ export const ErrorPointers = {
|
||||
OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token",
|
||||
GITHUB_APP_ID: "/data/attributes/secret/github_app_id",
|
||||
GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content",
|
||||
CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content",
|
||||
} as const;
|
||||
|
||||
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
|
||||
@@ -53,7 +53,7 @@ export const getProviderFormType = (
|
||||
via?: string,
|
||||
): ProviderFormType => {
|
||||
// Providers that need credential type selection
|
||||
const needsSelector = ["aws", "gcp", "github"].includes(providerType);
|
||||
const needsSelector = ["aws", "gcp", "github", "m365"].includes(providerType);
|
||||
|
||||
// Show selector if no via parameter and provider needs it
|
||||
if (needsSelector && !via) {
|
||||
@@ -80,6 +80,14 @@ export const getProviderFormType = (
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
// M365 credential types
|
||||
if (
|
||||
providerType === "m365" &&
|
||||
["app_client_secret", "app_certificate"].includes(via || "")
|
||||
) {
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
// Other providers go directly to credentials form
|
||||
if (!needsSelector) {
|
||||
return "credentials";
|
||||
@@ -99,6 +107,8 @@ export const requiresBackButton = (via?: string | null): boolean => {
|
||||
"personal_access_token",
|
||||
"oauth_app",
|
||||
"github_app",
|
||||
"app_client_secret",
|
||||
"app_certificate",
|
||||
];
|
||||
|
||||
return validViaTypes.includes(via);
|
||||
|
||||
Generated
+390
-18
File diff suppressed because it is too large.
Load diff
+7
-6
@@ -15,10 +15,10 @@
|
||||
"format:check": "./node_modules/.bin/prettier --check ./app",
|
||||
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
|
||||
"prepare": "husky",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e": "playwright test --project=chromium",
|
||||
"test:e2e:ui": "playwright test --project=chromium --ui",
|
||||
"test:e2e:debug": "playwright test --project=chromium --debug",
|
||||
"test:e2e:headed": "playwright test --project=chromium --headed",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e:install": "playwright install"
|
||||
},
|
||||
@@ -70,11 +70,11 @@
|
||||
"recharts": "2.15.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"server-only": "0.0.1",
|
||||
"shadcn": "3.2.1",
|
||||
"sharp": "0.33.5",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"uuid": "11.1.0",
|
||||
"world-atlas": "2.0.2",
|
||||
"zod": "4.1.11",
|
||||
@@ -82,8 +82,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "5.2.1",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@types/d3": "7.4.3",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
@@ -110,6 +110,7 @@
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-tailwindcss": "0.6.14",
|
||||
"shadcn": "3.4.1",
|
||||
"tailwind-variants": "0.1.20",
|
||||
"tailwindcss": "4.1.13",
|
||||
"typescript": "5.5.4"
|
||||
|
||||
@@ -20,6 +20,72 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
projects: [
|
||||
// ===========================================
|
||||
// Authentication Setup Projects
|
||||
// ===========================================
|
||||
// These projects handle user authentication for different permission levels
|
||||
// Each setup creates authenticated state files that can be reused by test suites
|
||||
|
||||
// Admin user authentication setup
|
||||
// Creates authenticated state for admin users with full system permissions
|
||||
{
|
||||
name: "admin.auth.setup",
|
||||
testMatch: "admin.auth.setup.ts",
|
||||
},
|
||||
|
||||
// Scans management user authentication setup
|
||||
// Creates authenticated state for users with scan management permissions
|
||||
{
|
||||
name: "manage-scans.auth.setup",
|
||||
testMatch: "manage-scans.auth.setup.ts",
|
||||
},
|
||||
|
||||
// Integrations management user authentication setup
|
||||
// Creates authenticated state for users with integration management permissions
|
||||
{
|
||||
name: "manage-integrations.auth.setup",
|
||||
testMatch: "manage-integrations.auth.setup.ts",
|
||||
},
|
||||
|
||||
// Account management user authentication setup
|
||||
// Creates authenticated state for users with account management permissions
|
||||
{
|
||||
name: "manage-account.auth.setup",
|
||||
testMatch: "manage-account.auth.setup.ts",
|
||||
},
|
||||
|
||||
// Cloud providers management user authentication setup
|
||||
// Creates authenticated state for users with cloud provider management permissions
|
||||
{
|
||||
name: "manage-cloud-providers.auth.setup",
|
||||
testMatch: "manage-cloud-providers.auth.setup.ts",
|
||||
},
|
||||
|
||||
// Unlimited visibility user authentication setup
|
||||
// Creates authenticated state for users with unlimited visibility permissions
|
||||
{
|
||||
name: "unlimited-visibility.auth.setup",
|
||||
testMatch: "unlimited-visibility.auth.setup.ts",
|
||||
},
|
||||
|
||||
// Invite and manage users authentication setup
|
||||
// Creates authenticated state for users with user invitation and management permissions
|
||||
{
|
||||
name: "invite-and-manage-users.auth.setup",
|
||||
testMatch: "invite-and-manage-users.auth.setup.ts",
|
||||
},
|
||||
|
||||
// All authentication setups combined
|
||||
// Runs all authentication setup files to create all user states
|
||||
{
|
||||
name: "all.auth.setup",
|
||||
testMatch: "**/*.auth.setup.ts",
|
||||
},
|
||||
|
||||
// ===========================================
|
||||
// Test Suite Projects
|
||||
// ===========================================
|
||||
// These projects run the actual test suites
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
|
||||
+11
-1
@@ -5,12 +5,15 @@ interface UIStoreState {
|
||||
isSideMenuOpen: boolean;
|
||||
isMutelistModalOpen: boolean;
|
||||
hasProviders: boolean;
|
||||
shouldAutoOpenMutelist: boolean;
|
||||
|
||||
openSideMenu: () => void;
|
||||
closeSideMenu: () => void;
|
||||
openMutelistModal: () => void;
|
||||
closeMutelistModal: () => void;
|
||||
setHasProviders: (value: boolean) => void;
|
||||
requestMutelistModalOpen: () => void;
|
||||
resetMutelistModalRequest: () => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIStoreState>()(
|
||||
@@ -19,11 +22,18 @@ export const useUIStore = create<UIStoreState>()(
|
||||
isSideMenuOpen: false,
|
||||
isMutelistModalOpen: false,
|
||||
hasProviders: false,
|
||||
shouldAutoOpenMutelist: false,
|
||||
openSideMenu: () => set({ isSideMenuOpen: true }),
|
||||
closeSideMenu: () => set({ isSideMenuOpen: false }),
|
||||
openMutelistModal: () => set({ isMutelistModalOpen: true }),
|
||||
openMutelistModal: () =>
|
||||
set({
|
||||
isMutelistModalOpen: true,
|
||||
shouldAutoOpenMutelist: false,
|
||||
}),
|
||||
closeMutelistModal: () => set({ isMutelistModalOpen: false }),
|
||||
setHasProviders: (value: boolean) => set({ hasProviders: value }),
|
||||
requestMutelistModalOpen: () => set({ shouldAutoOpenMutelist: true }),
|
||||
resetMutelistModalRequest: () => set({ shouldAutoOpenMutelist: false }),
|
||||
}),
|
||||
{
|
||||
name: "ui-store",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { SignInPage, SignInCredentials } from "./page-objects/sign-in-page";
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
INVALID_CREDENTIALS: "Invalid email or password",
|
||||
@@ -138,6 +139,29 @@ export async function verifyDashboardRoute(page: Page) {
|
||||
await expect(page).toHaveURL("/");
|
||||
}
|
||||
|
||||
export async function authenticateAndSaveState(
|
||||
page: Page,
|
||||
email: string,
|
||||
password: string,
|
||||
storagePath: string,
|
||||
) {
|
||||
if (!email || !password) {
|
||||
throw new Error('Email and password are required for authentication and save state');
|
||||
}
|
||||
|
||||
// Create SignInPage instance
|
||||
const signInPage = new SignInPage(page);
|
||||
const credentials: SignInCredentials = { email, password };
|
||||
|
||||
// Perform authentication steps using Page Object Model
|
||||
await signInPage.goto();
|
||||
await signInPage.login(credentials);
|
||||
await signInPage.verifySuccessfulLogin();
|
||||
|
||||
// Save authentication state
|
||||
await page.context().storageState({ path: storagePath });
|
||||
}
|
||||
|
||||
export async function getSession(page: Page) {
|
||||
const response = await page.request.get("/api/auth/session");
|
||||
return response.json();
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
|
||||
export class HomePage {
|
||||
readonly page: Page;
|
||||
|
||||
// Main content elements
|
||||
readonly mainContent: Locator;
|
||||
readonly breadcrumbs: Locator;
|
||||
readonly overviewHeading: Locator;
|
||||
|
||||
// Navigation elements
|
||||
readonly navigationMenu: Locator;
|
||||
readonly userMenu: Locator;
|
||||
readonly signOutButton: Locator;
|
||||
|
||||
// Dashboard elements
|
||||
readonly dashboardCards: Locator;
|
||||
readonly overviewSection: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly themeToggle: Locator;
|
||||
readonly logo: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
|
||||
// Main content elements
|
||||
this.mainContent = page.locator("main");
|
||||
this.breadcrumbs = page.getByLabel("Breadcrumbs");
|
||||
this.overviewHeading = page.getByRole("heading", { name: "Overview", exact: true });
|
||||
|
||||
// Navigation elements
|
||||
this.navigationMenu = page.locator("nav");
|
||||
this.userMenu = page.getByRole("button", { name: /user menu/i });
|
||||
this.signOutButton = page.getByRole("button", { name: "Sign out" });
|
||||
|
||||
// Dashboard elements
|
||||
this.dashboardCards = page.locator('[data-testid="dashboard-card"]');
|
||||
this.overviewSection = page.locator('[data-testid="overview-section"]');
|
||||
|
||||
// UI elements
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
this.logo = page.locator('svg[width="300"]');
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/");
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
// Verification methods
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/");
|
||||
await expect(this.mainContent).toBeVisible();
|
||||
await expect(this.overviewHeading).toBeVisible();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async verifyBreadcrumbs(): Promise<void> {
|
||||
await expect(this.breadcrumbs).toBeVisible();
|
||||
await expect(this.overviewHeading).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyMainContent(): Promise<void> {
|
||||
await expect(this.mainContent).toBeVisible();
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async navigateToOverview(): Promise<void> {
|
||||
await this.overviewHeading.click();
|
||||
}
|
||||
|
||||
async openUserMenu(): Promise<void> {
|
||||
await this.userMenu.click();
|
||||
}
|
||||
|
||||
async signOut(): Promise<void> {
|
||||
await this.openUserMenu();
|
||||
await this.signOutButton.click();
|
||||
}
|
||||
|
||||
// Dashboard methods
|
||||
async verifyDashboardCards(): Promise<void> {
|
||||
await expect(this.dashboardCards.first()).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyOverviewSection(): Promise<void> {
|
||||
await expect(this.overviewSection).toBeVisible();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Accessibility methods
|
||||
async verifyKeyboardNavigation(): Promise<void> {
|
||||
// Test tab navigation through main elements
|
||||
await this.page.keyboard.press("Tab");
|
||||
await expect(this.themeToggle).toBeFocused();
|
||||
}
|
||||
|
||||
// Wait methods
|
||||
async waitForRedirect(expectedUrl: string): Promise<void> {
|
||||
await this.page.waitForURL(expectedUrl);
|
||||
}
|
||||
|
||||
async waitForContentLoad(): Promise<void> {
|
||||
await this.page.waitForFunction(() => {
|
||||
const main = document.querySelector("main");
|
||||
return main && main.offsetHeight > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import { Page, Locator, expect } from "@playwright/test";
|
||||
import { HomePage } from "./home-page";
|
||||
|
||||
export interface SignInCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface SocialAuthConfig {
|
||||
googleEnabled: boolean;
|
||||
githubEnabled: boolean;
|
||||
}
|
||||
|
||||
export class SignInPage {
|
||||
readonly page: Page;
|
||||
readonly homePage: HomePage;
|
||||
|
||||
// Form elements
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly loginButton: Locator;
|
||||
readonly form: Locator;
|
||||
|
||||
// Social authentication buttons
|
||||
readonly googleButton: Locator;
|
||||
readonly githubButton: Locator;
|
||||
readonly samlButton: Locator;
|
||||
|
||||
// Navigation elements
|
||||
readonly signUpLink: Locator;
|
||||
readonly backButton: Locator;
|
||||
|
||||
// UI elements
|
||||
readonly title: Locator;
|
||||
readonly logo: Locator;
|
||||
readonly themeToggle: Locator;
|
||||
|
||||
// Error messages
|
||||
readonly errorMessages: Locator;
|
||||
readonly loadingIndicator: Locator;
|
||||
|
||||
// SAML specific elements
|
||||
readonly samlModeTitle: Locator;
|
||||
readonly samlEmailInput: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.homePage = new HomePage(page);
|
||||
|
||||
// Form elements
|
||||
this.emailInput = page.getByLabel("Email");
|
||||
this.passwordInput = page.getByLabel("Password");
|
||||
this.loginButton = page.getByRole("button", { name: "Log in" });
|
||||
this.form = page.locator("form");
|
||||
|
||||
// Social authentication buttons
|
||||
this.googleButton = page.getByText("Continue with Google");
|
||||
this.githubButton = page.getByText("Continue with Github");
|
||||
this.samlButton = page.getByText("Continue with SAML SSO");
|
||||
|
||||
// Navigation elements
|
||||
this.signUpLink = page.getByRole("link", { name: "Sign up" });
|
||||
this.backButton = page.getByText("Back");
|
||||
|
||||
// UI elements
|
||||
this.title = page.getByText("Sign in", { exact: true });
|
||||
this.logo = page.locator('svg[width="300"]');
|
||||
this.themeToggle = page.getByLabel("Toggle theme");
|
||||
|
||||
// Error messages
|
||||
this.errorMessages = page.locator('[role="alert"], .error-message, [data-testid="error"]');
|
||||
this.loadingIndicator = page.getByText("Loading");
|
||||
|
||||
// SAML specific elements
|
||||
this.samlModeTitle = page.getByText("Sign in with SAML SSO");
|
||||
this.samlEmailInput = page.getByLabel("Email");
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/sign-in");
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async waitForPageLoad(): Promise<void> {
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
// Form interaction methods
|
||||
async fillEmail(email: string): Promise<void> {
|
||||
await this.emailInput.fill(email);
|
||||
}
|
||||
|
||||
async fillPassword(password: string): Promise<void> {
|
||||
await this.passwordInput.fill(password);
|
||||
}
|
||||
|
||||
async fillCredentials(credentials: SignInCredentials): Promise<void> {
|
||||
await this.fillEmail(credentials.email);
|
||||
await this.fillPassword(credentials.password);
|
||||
}
|
||||
|
||||
async submitForm(): Promise<void> {
|
||||
await this.loginButton.click();
|
||||
}
|
||||
|
||||
async login(credentials: SignInCredentials): Promise<void> {
|
||||
await this.fillCredentials(credentials);
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
// Social authentication methods
|
||||
async clickGoogleAuth(): Promise<void> {
|
||||
await this.googleButton.click();
|
||||
}
|
||||
|
||||
async clickGithubAuth(): Promise<void> {
|
||||
await this.githubButton.click();
|
||||
}
|
||||
|
||||
async clickSamlAuth(): Promise<void> {
|
||||
await this.samlButton.click();
|
||||
}
|
||||
|
||||
// SAML SSO methods
|
||||
async toggleSamlMode(): Promise<void> {
|
||||
await this.clickSamlAuth();
|
||||
}
|
||||
|
||||
async goBackFromSaml(): Promise<void> {
|
||||
await this.backButton.click();
|
||||
}
|
||||
|
||||
async fillSamlEmail(email: string): Promise<void> {
|
||||
await this.samlEmailInput.fill(email);
|
||||
}
|
||||
|
||||
async submitSamlForm(): Promise<void> {
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
// Navigation methods
|
||||
async goToSignUp(): Promise<void> {
|
||||
await this.signUpLink.click();
|
||||
}
|
||||
|
||||
// Validation and assertion methods
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
await expect(this.page).toHaveTitle(/Prowler/);
|
||||
await expect(this.logo).toBeVisible();
|
||||
await expect(this.title).toBeVisible();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async verifyFormElements(): Promise<void> {
|
||||
await expect(this.emailInput).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
await expect(this.loginButton).toBeVisible();
|
||||
}
|
||||
|
||||
async verifySocialButtons(config: SocialAuthConfig): Promise<void> {
|
||||
if (config.googleEnabled) {
|
||||
await expect(this.googleButton).toBeVisible();
|
||||
}
|
||||
if (config.githubEnabled) {
|
||||
await expect(this.githubButton).toBeVisible();
|
||||
}
|
||||
await expect(this.samlButton).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyNavigationLinks(): Promise<void> {
|
||||
await expect(this.page.getByText("Need to create an account?")).toBeVisible();
|
||||
await expect(this.signUpLink).toBeVisible();
|
||||
}
|
||||
|
||||
async verifySuccessfulLogin(): Promise<void> {
|
||||
await this.homePage.verifyPageLoaded();
|
||||
}
|
||||
|
||||
async verifyLoginError(errorMessage: string = "Invalid email or password"): Promise<void> {
|
||||
await expect(this.page.getByText(errorMessage).first()).toBeVisible();
|
||||
await expect(this.page).toHaveURL("/sign-in");
|
||||
}
|
||||
|
||||
async verifySamlModeActive(): Promise<void> {
|
||||
await expect(this.samlModeTitle).toBeVisible();
|
||||
await expect(this.passwordInput).not.toBeVisible();
|
||||
await expect(this.backButton).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyNormalModeActive(): Promise<void> {
|
||||
await expect(this.title).toBeVisible();
|
||||
await expect(this.passwordInput).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyLoadingState(): Promise<void> {
|
||||
await expect(this.loginButton).toHaveAttribute("aria-disabled", "true");
|
||||
await expect(this.loadingIndicator).toBeVisible();
|
||||
}
|
||||
|
||||
async verifyFormValidation(): Promise<void> {
|
||||
// Check for common validation messages
|
||||
const emailError = this.page.getByText("Please enter a valid email address.");
|
||||
const passwordError = this.page.getByText("Password is required.");
|
||||
|
||||
// At least one validation error should be visible
|
||||
await expect(emailError.or(passwordError)).toBeVisible();
|
||||
}
|
||||
|
||||
// Accessibility methods
|
||||
async verifyKeyboardNavigation(): Promise<void> {
|
||||
// Test tab navigation through form elements
|
||||
await this.page.keyboard.press("Tab"); // Theme toggle
|
||||
await this.page.keyboard.press("Tab"); // Email field
|
||||
await expect(this.emailInput).toBeFocused();
|
||||
|
||||
await this.page.keyboard.press("Tab"); // Password field
|
||||
await expect(this.passwordInput).toBeFocused();
|
||||
|
||||
await this.page.keyboard.press("Tab"); // Show password button
|
||||
await this.page.keyboard.press("Tab"); // Login button
|
||||
await expect(this.loginButton).toBeFocused();
|
||||
}
|
||||
|
||||
async verifyAriaLabels(): Promise<void> {
|
||||
await expect(this.page.getByRole("textbox", { name: "Email" })).toBeVisible();
|
||||
await expect(this.page.getByRole("textbox", { name: "Password" })).toBeVisible();
|
||||
await expect(this.page.getByRole("button", { name: "Log in" })).toBeVisible();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
async clearForm(): Promise<void> {
|
||||
await this.emailInput.clear();
|
||||
await this.passwordInput.clear();
|
||||
}
|
||||
|
||||
async isFormValid(): Promise<boolean> {
|
||||
const emailValue = await this.emailInput.inputValue();
|
||||
const passwordValue = await this.passwordInput.inputValue();
|
||||
return emailValue.length > 0 && passwordValue.length > 0;
|
||||
}
|
||||
|
||||
async getFormErrors(): Promise<string[]> {
|
||||
const errorElements = await this.errorMessages.all();
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const element of errorElements) {
|
||||
const text = await element.textContent();
|
||||
if (text) {
|
||||
errors.push(text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Browser interaction methods
|
||||
async refresh(): Promise<void> {
|
||||
await this.page.reload();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
async goBack(): Promise<void> {
|
||||
await this.page.goBack();
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// Session management methods
|
||||
async logout(): Promise<void> {
|
||||
await this.homePage.signOut();
|
||||
}
|
||||
|
||||
async verifyLogoutSuccess(): Promise<void> {
|
||||
await expect(this.page).toHaveURL("/sign-in");
|
||||
await expect(this.title).toBeVisible();
|
||||
}
|
||||
|
||||
// Advanced interaction methods
|
||||
async fillFormWithValidation(credentials: SignInCredentials): Promise<void> {
|
||||
// Fill email first and check for validation
|
||||
await this.fillEmail(credentials.email);
|
||||
await this.page.keyboard.press("Tab"); // Trigger validation
|
||||
|
||||
// Fill password
|
||||
await this.fillPassword(credentials.password);
|
||||
}
|
||||
|
||||
async submitFormWithEnterKey(): Promise<void> {
|
||||
await this.passwordInput.press("Enter");
|
||||
}
|
||||
|
||||
async submitFormWithButtonClick(): Promise<void> {
|
||||
await this.submitForm();
|
||||
}
|
||||
|
||||
// Error handling methods
|
||||
async handleSamlError(): Promise<void> {
|
||||
const samlError = this.page.getByText("SAML Authentication Error");
|
||||
if (await samlError.isVisible()) {
|
||||
// Handle SAML error if present
|
||||
console.log("SAML authentication error detected");
|
||||
}
|
||||
}
|
||||
|
||||
// Wait methods
|
||||
async waitForFormSubmission(): Promise<void> {
|
||||
await this.page.waitForFunction(() => {
|
||||
const button = document.querySelector('button[aria-disabled="true"]');
|
||||
return button === null;
|
||||
});
|
||||
}
|
||||
|
||||
async waitForRedirect(expectedUrl: string): Promise<void> {
|
||||
await this.page.waitForURL(expectedUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authAdminSetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const adminUserFile = 'playwright/.auth/admin_user.json';
|
||||
|
||||
authAdminSetup('authenticate as admin e2e user', async ({ page }) => {
|
||||
const adminEmail = process.env.E2E_ADMIN_USER;
|
||||
const adminPassword = process.env.E2E_ADMIN_PASSWORD;
|
||||
|
||||
if (!adminEmail || !adminPassword) {
|
||||
throw new Error('E2E_ADMIN_USER and E2E_ADMIN_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, adminEmail, adminPassword, adminUserFile);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authInviteAndManageUsersSetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const inviteAndManageUsersUserFile = 'playwright/.auth/invite_and_manage_users_user.json';
|
||||
|
||||
authInviteAndManageUsersSetup('authenticate as invite and manage users e2e user', async ({ page }) => {
|
||||
const inviteAndManageUsersEmail = process.env.E2E_INVITE_AND_MANAGE_USERS_USER;
|
||||
const inviteAndManageUsersPassword = process.env.E2E_INVITE_AND_MANAGE_USERS_PASSWORD;
|
||||
|
||||
if (!inviteAndManageUsersEmail || !inviteAndManageUsersPassword) {
|
||||
throw new Error('E2E_INVITE_AND_MANAGE_USERS_USER and E2E_INVITE_AND_MANAGE_USERS_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, inviteAndManageUsersEmail, inviteAndManageUsersPassword, inviteAndManageUsersUserFile);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authManageAccountSetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const manageAccountUserFile = 'playwright/.auth/manage_account_user.json';
|
||||
|
||||
authManageAccountSetup('authenticate as manage account e2e user', async ({ page }) => {
|
||||
const accountEmail = process.env.E2E_MANAGE_ACCOUNT_USER;
|
||||
const accountPassword = process.env.E2E_MANAGE_ACCOUNT_PASSWORD;
|
||||
|
||||
if (!accountEmail || !accountPassword) {
|
||||
throw new Error('E2E_MANAGE_ACCOUNT_USER and E2E_MANAGE_ACCOUNT_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, accountEmail, accountPassword, manageAccountUserFile);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authManageCloudProvidersSetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const manageCloudProvidersUserFile = 'playwright/.auth/manage_cloud_providers_user.json';
|
||||
|
||||
authManageCloudProvidersSetup('authenticate as manage cloud providers e2e user', async ({ page }) => {
|
||||
const cloudProvidersEmail = process.env.E2E_MANAGE_CLOUD_PROVIDERS_USER;
|
||||
const cloudProvidersPassword = process.env.E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD;
|
||||
|
||||
if (!cloudProvidersEmail || !cloudProvidersPassword) {
|
||||
throw new Error('E2E_MANAGE_CLOUD_PROVIDERS_USER and E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, cloudProvidersEmail, cloudProvidersPassword, manageCloudProvidersUserFile);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authManageIntegrationsSetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const manageIntegrationsUserFile = 'playwright/.auth/manage_integrations_user.json';
|
||||
|
||||
authManageIntegrationsSetup('authenticate as integrations e2e user', async ({ page }) => {
|
||||
const integrationsEmail = process.env.E2E_MANAGE_INTEGRATIONS_USER;
|
||||
const integrationsPassword = process.env.E2E_MANAGE_INTEGRATIONS_PASSWORD;
|
||||
|
||||
if (!integrationsEmail || !integrationsPassword) {
|
||||
throw new Error('E2E_MANAGE_INTEGRATIONS_USER and E2E_MANAGE_INTEGRATIONS_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, integrationsEmail, integrationsPassword, manageIntegrationsUserFile);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authManageScansSetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const manageScansUserFile = 'playwright/.auth/manage_scans_user.json';
|
||||
|
||||
authManageScansSetup('authenticate as scans e2e user', async ({ page }) => {
|
||||
const scansEmail = process.env.E2E_MANAGE_SCANS_USER;
|
||||
const scansPassword = process.env.E2E_MANAGE_SCANS_PASSWORD;
|
||||
|
||||
if (!scansEmail || !scansPassword) {
|
||||
throw new Error('E2E_MANAGE_SCANS_USER and E2E_MANAGE_SCANS_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, scansEmail, scansPassword, manageScansUserFile);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { test as authUnlimitedVisibilitySetup } from '@playwright/test';
|
||||
import { authenticateAndSaveState } from '@/tests/helpers';
|
||||
|
||||
const unlimitedVisibilityUserFile = 'playwright/.auth/unlimited_visibility_user.json';
|
||||
|
||||
authUnlimitedVisibilitySetup('authenticate as unlimited visibility e2e user', async ({ page }) => {
|
||||
const unlimitedVisibilityEmail = process.env.E2E_UNLIMITED_VISIBILITY_USER;
|
||||
const unlimitedVisibilityPassword = process.env.E2E_UNLIMITED_VISIBILITY_PASSWORD;
|
||||
|
||||
if (!unlimitedVisibilityEmail || !unlimitedVisibilityPassword) {
|
||||
throw new Error('E2E_UNLIMITED_VISIBILITY_USER and E2E_UNLIMITED_VISIBILITY_PASSWORD environment variables are required');
|
||||
}
|
||||
|
||||
await authenticateAndSaveState(page, unlimitedVisibilityEmail, unlimitedVisibilityPassword, unlimitedVisibilityUserFile);
|
||||
});
|
||||
+14
-5
@@ -1,5 +1,5 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { SVGProps } from "react";
|
||||
import { MouseEvent, SVGProps } from "react";
|
||||
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
|
||||
@@ -21,7 +21,7 @@ export type SubmenuProps = {
|
||||
active?: boolean;
|
||||
icon: IconComponent;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
|
||||
export type MenuProps = {
|
||||
@@ -213,15 +213,24 @@ export type AzureCredentials = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365Credentials = {
|
||||
export type M365ClientSecretCredentials = {
|
||||
[ProviderCredentialFields.CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: string;
|
||||
[ProviderCredentialFields.TENANT_ID]: string;
|
||||
[ProviderCredentialFields.USER]?: string;
|
||||
[ProviderCredentialFields.PASSWORD]?: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365CertificateCredentials = {
|
||||
[ProviderCredentialFields.CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: string;
|
||||
[ProviderCredentialFields.TENANT_ID]: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365Credentials =
|
||||
| M365ClientSecretCredentials
|
||||
| M365CertificateCredentials;
|
||||
|
||||
export type GCPDefaultCredentials = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
|
||||
+30
-26
@@ -168,12 +168,13 @@ export const addCredentialsFormSchema = (
|
||||
.min(1, "Client ID is required"),
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: z
|
||||
.string()
|
||||
.min(1, "Client Secret is required"),
|
||||
.optional(),
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
|
||||
.string()
|
||||
.optional(),
|
||||
[ProviderCredentialFields.TENANT_ID]: z
|
||||
.string()
|
||||
.min(1, "Tenant ID is required"),
|
||||
[ProviderCredentialFields.USER]: z.string().optional(),
|
||||
[ProviderCredentialFields.PASSWORD]: z.string().optional(),
|
||||
}
|
||||
: providerType === "github"
|
||||
? {
|
||||
@@ -194,23 +195,26 @@ export const addCredentialsFormSchema = (
|
||||
})
|
||||
.superRefine((data: Record<string, any>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
const hasUser = !!data[ProviderCredentialFields.USER];
|
||||
const hasPassword = !!data[ProviderCredentialFields.PASSWORD];
|
||||
|
||||
if (hasUser && !hasPassword) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "If you provide a user, you must also provide a password",
|
||||
path: [ProviderCredentialFields.PASSWORD],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPassword && !hasUser) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "If you provide a password, you must also provide a user",
|
||||
path: [ProviderCredentialFields.USER],
|
||||
});
|
||||
// Validate based on the via parameter
|
||||
if (via === "app_client_secret") {
|
||||
const clientSecret = data[ProviderCredentialFields.CLIENT_SECRET];
|
||||
if (!clientSecret || clientSecret.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Client Secret is required",
|
||||
path: [ProviderCredentialFields.CLIENT_SECRET],
|
||||
});
|
||||
}
|
||||
} else if (via === "app_certificate") {
|
||||
const certificateContent =
|
||||
data[ProviderCredentialFields.CERTIFICATE_CONTENT];
|
||||
if (!certificateContent || certificateContent.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Certificate Content is required",
|
||||
path: [ProviderCredentialFields.CERTIFICATE_CONTENT],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +223,7 @@ export const addCredentialsFormSchema = (
|
||||
if (via === "personal_access_token") {
|
||||
if (!data[ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "Personal Access Token is required",
|
||||
path: [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN],
|
||||
});
|
||||
@@ -227,7 +231,7 @@ export const addCredentialsFormSchema = (
|
||||
} else if (via === "oauth_app") {
|
||||
if (!data[ProviderCredentialFields.OAUTH_APP_TOKEN]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "OAuth App Token is required",
|
||||
path: [ProviderCredentialFields.OAUTH_APP_TOKEN],
|
||||
});
|
||||
@@ -235,14 +239,14 @@ export const addCredentialsFormSchema = (
|
||||
} else if (via === "github_app") {
|
||||
if (!data[ProviderCredentialFields.GITHUB_APP_ID]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "GitHub App ID is required",
|
||||
path: [ProviderCredentialFields.GITHUB_APP_ID],
|
||||
});
|
||||
}
|
||||
if (!data[ProviderCredentialFields.GITHUB_APP_KEY]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "GitHub App Private Key is required",
|
||||
path: [ProviderCredentialFields.GITHUB_APP_KEY],
|
||||
});
|
||||
@@ -390,7 +394,7 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
const yamlValidation = validateYaml(val);
|
||||
if (!yamlValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: `Invalid YAML format: ${yamlValidation.error}`,
|
||||
});
|
||||
return;
|
||||
@@ -399,7 +403,7 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
const mutelistValidation = validateMutelistYaml(val);
|
||||
if (!mutelistValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: `Invalid mutelist structure: ${mutelistValidation.error}`,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user