mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-19 02:21:52 +00:00
feat(ui): migrate from HeroUI to shadcn/ui (#11532)
This commit is contained in:
@@ -158,7 +158,7 @@ The MCP Server has a small direct-dependency surface and does not yet declare a
|
||||
The UI uses [pnpm](https://pnpm.io) with supply-chain controls configured in [`ui/pnpm-workspace.yaml`](https://github.com/prowler-cloud/prowler/blob/master/ui/pnpm-workspace.yaml).
|
||||
|
||||
- **Minimum release age** (`minimumReleaseAge: 1440`): packages must publish at least 24 hours before install. This reduces exposure during the window when a compromised release has not yet been detected and yanked.
|
||||
- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `@heroui/shared-utils`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install.
|
||||
- **Lifecycle script allow-list** (`strictDepBuilds: true` + `allowBuilds`): only explicitly approved packages may run `install` or `postinstall` scripts (currently `sharp`, `esbuild`, `@sentry/cli`, `unrs-resolver`, `msw`). Any unlisted package with lifecycle scripts fails the install.
|
||||
- **Trust policy** (`trustPolicy: no-downgrade`): the install fails when a package's trust evidence drops, for example after a new publisher takes over.
|
||||
- **Block exotic subdeps** (`blockExoticSubdeps: true`): transitive dependencies cannot ship as git URLs or tarballs. Every package in the tree resolves from the configured registry.
|
||||
- **Transitive overrides** in [`ui/package.json`](https://github.com/prowler-cloud/prowler/blob/master/ui/package.json) force specific versions for transitive packages (`lodash`, `serialize-javascript`, `qs`, `rollup`, `minimatch`, `ajv`, and others).
|
||||
|
||||
+19
-24
@@ -2,11 +2,11 @@
|
||||
name: prowler-ui
|
||||
description: >
|
||||
Prowler UI-specific patterns. For generic patterns, see: typescript, react-19, nextjs-16, tailwind-4.
|
||||
Trigger: When working inside ui/ on Prowler-specific conventions (shadcn vs HeroUI legacy, folder placement, actions/adapters, shared types/hooks/lib).
|
||||
Trigger: When working inside ui/ on Prowler-specific conventions (shadcn, folder placement, actions/adapters, shared types/hooks/lib).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "1.0"
|
||||
version: "1.1"
|
||||
scope: [root, ui]
|
||||
auto_invoke:
|
||||
- "Creating/modifying Prowler UI components"
|
||||
@@ -32,13 +32,12 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
|
||||
Next.js 16.2.3 | React 19.2.5 | Tailwind 4.1.18 | shadcn/ui
|
||||
Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8
|
||||
NextAuth 5.0.0-beta.30 | Recharts 2.15.4
|
||||
HeroUI 2.8.4 (LEGACY - do not add new components)
|
||||
```
|
||||
|
||||
## CRITICAL: Component Library Rule
|
||||
|
||||
- **ALWAYS**: Use `shadcn/ui` + Tailwind (`components/shadcn/`)
|
||||
- **NEVER**: Add new HeroUI components (`components/ui/` is legacy only)
|
||||
- **NEVER**: Add components to `components/ui/` (temporary re-export shims for the prowler-cloud overlay only)
|
||||
|
||||
## Design System Discipline (REQUIRED)
|
||||
|
||||
@@ -57,12 +56,11 @@ When reviewing UI PRs, flag: custom modals/primitives that duplicate shadcn, cal
|
||||
### Component Placement
|
||||
|
||||
```text
|
||||
New feature UI? → shadcn/ui + Tailwind
|
||||
Existing HeroUI feature? → Keep HeroUI (don't mix)
|
||||
Used 1 feature? → features/{feature}/components/
|
||||
Used 2+ features? → components/shared/
|
||||
Needs state/hooks? → "use client"
|
||||
Server component? → No directive needed
|
||||
New UI primitive? → components/shadcn/ (shadcn/ui + Tailwind)
|
||||
Used by 1 domain? → components/{domain}/
|
||||
Used by 2+ domains? → components/shared/
|
||||
Needs state/hooks? → "use client"
|
||||
Server component? → No directive needed
|
||||
```
|
||||
|
||||
### Code Location
|
||||
@@ -76,10 +74,16 @@ Utils (shared 2+) → lib/
|
||||
Utils (local 1) → {feature}/utils/
|
||||
Hooks (shared 2+) → hooks/
|
||||
Hooks (local 1) → {feature}/hooks.ts
|
||||
shadcn components → components/shadcn/
|
||||
HeroUI components → components/ui/ (LEGACY)
|
||||
UI primitive → components/shadcn/
|
||||
Domain component → components/{domain}/
|
||||
```
|
||||
|
||||
> **Deprecated:** `components/ui/` is a temporary re-export shim that maps
|
||||
> legacy import paths to `components/shadcn/` for the prowler-cloud overlay.
|
||||
> HeroUI is fully removed. Never add or import components here — use
|
||||
> `@/components/shadcn` (primitives) or `@/components/{domain}` instead.
|
||||
> Delete the shim once the cloud repo migrates to `@/components/shadcn`.
|
||||
|
||||
### Styling Decision
|
||||
|
||||
```text
|
||||
@@ -110,8 +114,9 @@ ui/
|
||||
│ ├── services/
|
||||
│ └── integrations/
|
||||
├── components/
|
||||
│ ├── shadcn/ # shadcn/ui (USE THIS)
|
||||
│ ├── ui/ # HeroUI (LEGACY)
|
||||
│ ├── shadcn/ # shadcn/ui primitives (USE THIS)
|
||||
│ ├── shared/ # Cross-domain composed components (2+ domains)
|
||||
│ ├── ui/ # DEPRECATED shim → re-exports shadcn (do not use)
|
||||
│ ├── {domain}/ # Domain-specific (compliance, findings, providers, etc.)
|
||||
│ ├── filters/ # Filter components
|
||||
│ ├── graphs/ # Chart components
|
||||
@@ -324,16 +329,6 @@ Before requesting re-review from a reviewer:
|
||||
- [ ] If you disagreed: the reply explains why with clear reasoning — do not leave threads silently open
|
||||
- [ ] Re-request review only after all threads are in a clean state
|
||||
|
||||
## Migrations Reference
|
||||
|
||||
| From | To | Key Changes |
|
||||
|------|-----|-------------|
|
||||
| React 18 | 19.1 | Async components, React Compiler (no useMemo/useCallback) |
|
||||
| Next.js 14 | 15.5 | Improved App Router, better streaming |
|
||||
| NextUI | HeroUI 2.8.4 | Package rename only, same API |
|
||||
| Zod 3 | 4 | `z.email()` not `z.string().email()`, `error` not `message` |
|
||||
| AI SDK 4 | 5 | `@ai-sdk/react`, `sendMessage` not `handleSubmit`, `parts` not `content` |
|
||||
|
||||
## Resources
|
||||
|
||||
- **Documentation**: See [references/](references/) for links to local developer guide
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
+4
-4
@@ -97,8 +97,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
||||
### Component Placement
|
||||
|
||||
```text
|
||||
New/Existing UI? → shadcn/ui + Tailwind (NEVER HeroUI for new code)
|
||||
Used 1 feature? → features/{feature}/components | Used 2+? → components/{domain}/
|
||||
New UI primitive? → components/shadcn/ (shadcn/ui + Tailwind)
|
||||
Used by 1 domain? → components/{domain}/ | Used by 2+ domains? → components/shared/
|
||||
Needs state/hooks? → "use client" | Server component? → No directive
|
||||
```
|
||||
|
||||
@@ -194,7 +194,7 @@ test("action works", { tag: ["@critical", "@feature"] }, async ({ page }) => {
|
||||
Next.js 16.2.3 | React 19.2.5 | Tailwind 4.1.18 | shadcn/ui
|
||||
Zod 4.1.11 | React Hook Form 7.62.0 | Zustand 5.0.8 | NextAuth 5.0.0-beta.30 | Recharts 2.15.4
|
||||
|
||||
> **Note**: HeroUI exists in `components/ui/` as legacy code. Do NOT add new components there.
|
||||
> **Note**: `components/ui/` only holds temporary re-export shims for the prowler-cloud overlay. Do NOT add new components there.
|
||||
|
||||
---
|
||||
|
||||
@@ -205,7 +205,7 @@ ui/
|
||||
├── app/(auth)/ # Auth pages
|
||||
├── app/(prowler)/ # Main app: compliance, findings, providers, scans
|
||||
├── components/shadcn/ # shadcn/ui components (USE THIS)
|
||||
├── components/ui/ # HeroUI (LEGACY - do not add here)
|
||||
├── components/ui/ # Cloud-overlay re-export shims (do not add here)
|
||||
├── actions/ # Server actions
|
||||
├── types/ # Shared types
|
||||
├── hooks/ # Shared hooks
|
||||
|
||||
+3
-4
@@ -114,10 +114,9 @@ pnpm run dev
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [Next.js 14](https://nextjs.org/docs/getting-started)
|
||||
- [NextUI v2](https://nextui.org/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [Tailwind Variants](https://tailwind-variants.org)
|
||||
- [Next.js 16](https://nextjs.org/docs/getting-started)
|
||||
- [shadcn/ui](https://ui.shadcn.com/) (built on [Radix UI](https://www.radix-ui.com/))
|
||||
- [Tailwind CSS 4](https://tailwindcss.com/)
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Framer Motion](https://www.framer.com/motion/)
|
||||
- [next-themes](https://github.com/pacocoursey/next-themes)
|
||||
|
||||
@@ -16,7 +16,8 @@ vi.mock("@/components/auth/oss/auth-layout", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
Button: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ vi.mock("@/components/auth/oss/auth-layout", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
Button: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -74,11 +74,11 @@ export function AcceptInvitationClient({
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Icon
|
||||
icon="solar:danger-triangle-bold"
|
||||
className="text-warning"
|
||||
className="text-text-warning-primary"
|
||||
width={48}
|
||||
/>
|
||||
<h1 className="text-xl font-semibold">Invalid Invitation Link</h1>
|
||||
<p className="text-default-500">
|
||||
<p className="text-text-neutral-tertiary">
|
||||
No invitation token was provided. Please check the link you
|
||||
received.
|
||||
</p>
|
||||
@@ -93,11 +93,11 @@ export function AcceptInvitationClient({
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Icon
|
||||
icon="eos-icons:loading"
|
||||
className="text-default-500"
|
||||
className="text-text-neutral-tertiary"
|
||||
width={48}
|
||||
/>
|
||||
<h1 className="text-xl font-semibold">Accepting Invitation...</h1>
|
||||
<p className="text-default-500">
|
||||
<p className="text-text-neutral-tertiary">
|
||||
Please wait while we process your invitation.
|
||||
</p>
|
||||
</div>
|
||||
@@ -108,13 +108,13 @@ export function AcceptInvitationClient({
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Icon
|
||||
icon="solar:danger-triangle-bold"
|
||||
className="text-danger"
|
||||
className="text-text-error-primary"
|
||||
width={48}
|
||||
/>
|
||||
<h1 className="text-xl font-semibold">
|
||||
Could Not Accept Invitation
|
||||
</h1>
|
||||
<p className="text-default-500">{state.message}</p>
|
||||
<p className="text-text-neutral-tertiary">{state.message}</p>
|
||||
<div className="flex gap-3">
|
||||
{state.canRetry && <Button onClick={doAccept}>Retry</Button>}
|
||||
<Button asChild variant="outline">
|
||||
@@ -129,14 +129,14 @@ export function AcceptInvitationClient({
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<Icon
|
||||
icon="solar:letter-bold"
|
||||
className="text-primary"
|
||||
className="text-button-primary"
|
||||
width={48}
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">
|
||||
You've Been Invited
|
||||
</h1>
|
||||
<p className="text-default-500 mt-2">
|
||||
<p className="text-text-neutral-tertiary mt-2">
|
||||
You've been invited to join a tenant on Prowler. How would
|
||||
you like to continue?
|
||||
</p>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { connection } from "next/server";
|
||||
import { ReactNode, Suspense } from "react";
|
||||
|
||||
import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config";
|
||||
import { NavigationProgress, Toaster } from "@/components/ui";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import { NavigationProgress, Toaster } from "@/components/shadcn";
|
||||
import { fontMono, fontSans } from "@/config/fonts";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { cn } from "@/lib";
|
||||
import { readGatedEnv } from "@/lib/integrations";
|
||||
@@ -56,8 +56,9 @@ export default async function AuthLayout({
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"bg-background min-h-screen font-sans antialiased",
|
||||
"bg-bg-neutral-primary min-h-screen font-sans antialiased",
|
||||
fontSans.variable,
|
||||
fontMono.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getLatestFindings } from "@/actions/findings/findings";
|
||||
import { LinkToFindings } from "@/components/overview";
|
||||
import { ColumnLatestFindings } from "@/components/overview/new-findings-table/table";
|
||||
import { CardTitle } from "@/components/shadcn";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import { FINDINGS_FILTERED_SORT, MUTED_FILTER } from "@/lib";
|
||||
import { createDict } from "@/lib/utils";
|
||||
import { FindingProps, SearchParamsProps } from "@/types";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Skeleton } from "@heroui/skeleton";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { SkeletonTableNewFindings } from "@/components/overview/new-findings-table/table";
|
||||
import { Skeleton } from "@/components/shadcn";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { GraphsTabsClient } from "./_components/graphs-tabs-client";
|
||||
|
||||
@@ -40,7 +40,7 @@ vi.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/components/ui/entities/entity-info", () => ({
|
||||
vi.mock("@/components/shadcn/entities/entity-info", () => ({
|
||||
EntityInfo: ({
|
||||
entityAlias,
|
||||
entityId,
|
||||
|
||||
@@ -61,11 +61,9 @@ vi.mock("next/navigation", () => ({
|
||||
useSearchParams: () => new URLSearchParams(routerMocks.currentSearch),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useToast: () => ({ toast: toastMock }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn", () => ({
|
||||
Button: ({
|
||||
asChild,
|
||||
children,
|
||||
|
||||
@@ -22,7 +22,7 @@ vi.mock("next/navigation", () => ({
|
||||
useSearchParams: () => new URLSearchParams(navigationMocks.currentSearch),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/table/data-table", () => ({
|
||||
vi.mock("@/components/shadcn/table/data-table", () => ({
|
||||
DataTable: ({
|
||||
columns,
|
||||
data,
|
||||
@@ -81,7 +81,7 @@ vi.mock("@/components/ui/table/data-table", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/table/data-table-column-header", () => ({
|
||||
vi.mock("@/components/shadcn/table/data-table-column-header", () => ({
|
||||
DataTableColumnHeader: ({ title }: { title: string }) => <span>{title}</span>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ vi.mock("next/navigation", () => ({
|
||||
useRouter: () => routerMocks,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
ToastAction: ({
|
||||
asChild,
|
||||
children,
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
type AlertRule,
|
||||
} from "@/app/(prowler)/alerts/_types";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { DOCS_URLS } from "@/lib/external-urls";
|
||||
import type { MetaDataProps } from "@/types";
|
||||
import type { ScanEntity } from "@/types";
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
ActionDropdownDangerZone,
|
||||
ActionDropdownItem,
|
||||
} from "@/components/shadcn/dropdown";
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import { DataTable } from "@/components/ui/table/data-table";
|
||||
import { DataTableColumnHeader } from "@/components/ui/table/data-table-column-header";
|
||||
import { DateWithTime } from "@/components/shadcn/entities";
|
||||
import { DataTable } from "@/components/shadcn/table/data-table";
|
||||
import { DataTableColumnHeader } from "@/components/shadcn/table/data-table-column-header";
|
||||
import type { MetaDataProps } from "@/types";
|
||||
|
||||
interface AlertsTableProps {
|
||||
|
||||
@@ -28,8 +28,8 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn";
|
||||
import { ToastAction, useToast } from "@/components/shadcn";
|
||||
import { CloudFeatureBadgeLink } from "@/components/shared/cloud-feature-badge";
|
||||
import { ToastAction, useToast } from "@/components/ui";
|
||||
import type { ScanEntity } from "@/types";
|
||||
import type { ProviderProps } from "@/types/providers";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getAllProviders } from "@/actions/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import { getAlert, listAlerts } from "@/app/(prowler)/alerts/_actions";
|
||||
import { AlertsManager } from "@/app/(prowler)/alerts/_components/alerts-manager";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { createScanDetailsMapping } from "@/lib";
|
||||
import type { MetaDataProps, ScanEntity, ScanProps } from "@/types";
|
||||
|
||||
|
||||
@@ -128,45 +128,44 @@ export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
|
||||
"[--active-color:var(--step-color)]",
|
||||
"[--complete-background-color:var(--step-color)]",
|
||||
"[--complete-border-color:var(--step-color)]",
|
||||
"[--inactive-border-color:hsl(var(--heroui-default-300))]",
|
||||
"[--inactive-color:hsl(var(--heroui-default-300))]",
|
||||
"[--inactive-border-color:var(--border-neutral-tertiary)]",
|
||||
"[--inactive-color:var(--border-neutral-tertiary)]",
|
||||
];
|
||||
|
||||
switch (color) {
|
||||
case "primary":
|
||||
userColor = "[--step-color:hsl(var(--heroui-primary))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-primary-foreground))]";
|
||||
break;
|
||||
case "secondary":
|
||||
userColor = "[--step-color:hsl(var(--heroui-secondary))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-secondary-foreground))]";
|
||||
userColor =
|
||||
"[--step-color:var(--color-violet-600)] dark:[--step-color:var(--color-violet-400)]";
|
||||
fgColor = "[--step-fg-color:var(--color-white)]";
|
||||
break;
|
||||
case "success":
|
||||
userColor = "[--step-color:hsl(var(--heroui-success))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-success-foreground))]";
|
||||
userColor = "[--step-color:var(--bg-pass-primary)]";
|
||||
fgColor = "[--step-fg-color:var(--color-black)]";
|
||||
break;
|
||||
case "warning":
|
||||
userColor = "[--step-color:hsl(var(--heroui-warning))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-warning-foreground))]";
|
||||
userColor = "[--step-color:var(--bg-warning-primary)]";
|
||||
fgColor = "[--step-fg-color:var(--color-black)]";
|
||||
break;
|
||||
case "danger":
|
||||
userColor = "[--step-color:hsl(var(--heroui-error))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-error-foreground))]";
|
||||
userColor = "[--step-color:var(--bg-fail-primary)]";
|
||||
fgColor = "[--step-fg-color:var(--color-white)]";
|
||||
break;
|
||||
case "default":
|
||||
userColor = "[--step-color:hsl(var(--heroui-default))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-default-foreground))]";
|
||||
userColor =
|
||||
"[--step-color:var(--color-zinc-300)] dark:[--step-color:var(--color-zinc-600)]";
|
||||
fgColor = "[--step-fg-color:var(--text-neutral-primary)]";
|
||||
break;
|
||||
case "primary":
|
||||
default:
|
||||
userColor = "[--step-color:hsl(var(--heroui-primary))]";
|
||||
fgColor = "[--step-fg-color:hsl(var(--heroui-primary-foreground))]";
|
||||
userColor = "[--step-color:var(--bg-button-primary)]";
|
||||
fgColor = "[--step-fg-color:var(--color-black)]";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!className?.includes("--step-fg-color")) colorsVars.unshift(fgColor);
|
||||
if (!className?.includes("--step-color")) colorsVars.unshift(userColor);
|
||||
if (!className?.includes("--inactive-bar-color"))
|
||||
colorsVars.push("[--inactive-bar-color:hsl(var(--heroui-default-300))]");
|
||||
colorsVars.push("[--inactive-bar-color:var(--border-neutral-tertiary)]");
|
||||
|
||||
const colors = colorsVars;
|
||||
|
||||
@@ -189,7 +188,7 @@ export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
|
||||
ref={ref}
|
||||
aria-current={status === "active" ? "step" : undefined}
|
||||
className={cn(
|
||||
"group rounded-large flex w-full cursor-pointer items-center justify-center gap-4 px-3 py-2.5",
|
||||
"group flex w-full cursor-pointer items-center justify-center gap-4 rounded-[14px] px-3 py-2.5",
|
||||
stepClassName,
|
||||
)}
|
||||
onClick={() => setCurrentStep(stepIdx)}
|
||||
@@ -201,7 +200,7 @@ export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
|
||||
<m.div
|
||||
animate={status}
|
||||
className={cn(
|
||||
"border-medium text-large text-default-foreground relative flex h-[34px] w-[34px] items-center justify-center rounded-full font-semibold",
|
||||
"text-text-neutral-primary relative flex h-[34px] w-[34px] items-center justify-center rounded-full border-2 text-lg font-semibold",
|
||||
{
|
||||
"shadow-lg": status === "complete",
|
||||
},
|
||||
@@ -242,9 +241,10 @@ export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-medium text-default-foreground font-medium transition-[color,opacity] duration-300 group-active:opacity-70",
|
||||
"text-text-neutral-primary text-base font-medium transition-[color,opacity] duration-300 group-active:opacity-70",
|
||||
{
|
||||
"text-default-500": status === "inactive",
|
||||
"text-text-neutral-tertiary":
|
||||
status === "inactive",
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -252,9 +252,10 @@ export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"text-tiny text-default-600 lg:text-small transition-[color,opacity] duration-300 group-active:opacity-70",
|
||||
"text-text-neutral-secondary text-xs transition-[color,opacity] duration-300 group-active:opacity-70 lg:text-sm",
|
||||
{
|
||||
"text-default-500": status === "inactive",
|
||||
"text-text-neutral-tertiary":
|
||||
status === "inactive",
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const WorkflowAttackPaths = () => {
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<h3 className="dark:text-prowler-theme-pale/90 text-sm font-semibold">
|
||||
<h3 className="dark:text-text-neutral-primary/90 text-sm font-semibold">
|
||||
Step {currentStep + 1} of {steps.length}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import type { GraphNode } from "@/types/attack-paths";
|
||||
|
||||
import { NodeDetailPanel } from "./node-detail-panel";
|
||||
|
||||
vi.mock("@/components/ui/sheet/sheet", () => ({
|
||||
vi.mock("@/components/shadcn/sheet/sheet", () => ({
|
||||
Sheet: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SheetContent: ({ children }: { children: ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Card, CardContent } from "@/components/shadcn";
|
||||
import { Spinner } from "@/components/shadcn/spinner/spinner";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet/sheet";
|
||||
} from "@/components/shadcn/sheet/sheet";
|
||||
import { Spinner } from "@/components/shadcn/spinner/spinner";
|
||||
import type { GraphNode } from "@/types/attack-paths";
|
||||
|
||||
import { NodeFindings } from "./node-findings";
|
||||
@@ -46,7 +46,7 @@ export const NodeDetailContent = ({
|
||||
{/* Node Overview Section */}
|
||||
<Card className="border-border-neutral-secondary">
|
||||
<CardContent className="flex flex-col gap-3 p-4">
|
||||
<h3 className="dark:text-prowler-theme-pale/90 text-sm font-semibold">
|
||||
<h3 className="dark:text-text-neutral-primary/90 text-sm font-semibold">
|
||||
Node Overview
|
||||
</h3>
|
||||
<NodeOverview node={node} />
|
||||
@@ -57,7 +57,7 @@ export const NodeDetailContent = ({
|
||||
{!isProwlerFinding && (
|
||||
<Card className="border-border-neutral-secondary">
|
||||
<CardContent className="flex flex-col gap-3 p-4">
|
||||
<h3 className="dark:text-prowler-theme-pale/90 text-sm font-semibold">
|
||||
<h3 className="dark:text-text-neutral-primary/90 text-sm font-semibold">
|
||||
Related Findings
|
||||
</h3>
|
||||
<div className="text-text-neutral-secondary dark:text-text-neutral-secondary text-xs">
|
||||
@@ -77,7 +77,7 @@ export const NodeDetailContent = ({
|
||||
{isProwlerFinding && (
|
||||
<Card className="border-border-neutral-secondary">
|
||||
<CardContent className="flex flex-col gap-3 p-4">
|
||||
<h3 className="dark:text-prowler-theme-pale/90 text-sm font-semibold">
|
||||
<h3 className="dark:text-text-neutral-primary/90 text-sm font-semibold">
|
||||
Affected Resources
|
||||
</h3>
|
||||
<div className="text-text-neutral-secondary dark:text-text-neutral-secondary text-xs">
|
||||
@@ -112,7 +112,7 @@ export const NodeDetailPanel = ({
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose?.()}>
|
||||
<SheetContent className="dark:bg-prowler-theme-midnight my-4 max-h-[calc(100vh-2rem)] max-w-[95vw] overflow-y-auto rounded-l-xl pt-10 md:my-8 md:max-h-[calc(100vh-4rem)] md:max-w-[55vw]">
|
||||
<SheetContent className="my-4 max-h-[calc(100vh-2rem)] max-w-[95vw] overflow-y-auto rounded-l-xl pt-10 md:my-8 md:max-h-[calc(100vh-4rem)] md:max-w-[55vw]">
|
||||
<SheetHeader>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { Spinner } from "@/components/shadcn/spinner/spinner";
|
||||
import { SeverityBadge } from "@/components/ui/table/severity-badge";
|
||||
import { SeverityBadge } from "@/components/shadcn/table/severity-badge";
|
||||
import type { GraphNode } from "@/types/attack-paths";
|
||||
|
||||
const SEVERITY_LEVELS = {
|
||||
@@ -80,7 +80,7 @@ export const NodeFindings = ({
|
||||
severity={normalizeSeverity(finding.properties.severity)}
|
||||
/>
|
||||
)}
|
||||
<h5 className="dark:text-prowler-theme-pale/90 text-sm font-medium">
|
||||
<h5 className="dark:text-text-neutral-primary/90 text-sm font-medium">
|
||||
{findingName}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { CodeSnippet } from "@/components/shadcn/code-snippet/code-snippet";
|
||||
import { DateWithTime } from "@/components/shadcn/entities/date-with-time";
|
||||
import { InfoField } from "@/components/shadcn/info-field/info-field";
|
||||
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import type { GraphNode, GraphNodePropertyValue } from "@/types/attack-paths";
|
||||
|
||||
import { formatNodeLabels } from "../../_lib";
|
||||
@@ -47,7 +47,7 @@ export const NodeOverview = ({ node }: NodeOverviewProps) => {
|
||||
|
||||
{/* Display all properties */}
|
||||
<div className="mt-4 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<h4 className="dark:text-prowler-theme-pale/90 mb-3 text-sm font-semibold">
|
||||
<h4 className="dark:text-text-neutral-primary/90 mb-3 text-sm font-semibold">
|
||||
Properties
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
|
||||
+3
-10
@@ -3,6 +3,7 @@
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import { Spinner } from "@/components/shadcn/spinner/spinner";
|
||||
import { StatusFindingBadge } from "@/components/shadcn/table";
|
||||
|
||||
interface Finding {
|
||||
id: string;
|
||||
@@ -40,12 +41,6 @@ export const NodeRemediation = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
if (status === "PASS") return "default";
|
||||
if (status === "FAIL") return "destructive";
|
||||
return "secondary";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{findings.map((finding) => (
|
||||
@@ -55,7 +50,7 @@ export const NodeRemediation = ({
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1">
|
||||
<h5 className="dark:text-prowler-theme-pale/90 text-sm font-medium">
|
||||
<h5 className="dark:text-text-neutral-primary/90 text-sm font-medium">
|
||||
{finding.title}
|
||||
</h5>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
@@ -66,9 +61,7 @@ export const NodeRemediation = ({
|
||||
<Badge variant={getSeverityVariant(finding.severity)}>
|
||||
{finding.severity}
|
||||
</Badge>
|
||||
<Badge variant={getStatusVariant(finding.status)}>
|
||||
{finding.status}
|
||||
</Badge>
|
||||
<StatusFindingBadge status={finding.status} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export const NodeResources = ({ node, allNodes = [] }: NodeResourcesProps) => {
|
||||
{resource.labels[0]}
|
||||
</Badge>
|
||||
)}
|
||||
<h5 className="dark:text-prowler-theme-pale/90 text-sm font-medium">
|
||||
<h5 className="dark:text-text-neutral-primary/90 text-sm font-medium">
|
||||
{String(resource.properties?.name || resourceId)}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
} from "@/components/shadcn/form";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type AttackPathQuery,
|
||||
@@ -37,7 +37,7 @@ export const QueryParametersForm = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="dark:text-prowler-theme-pale/90 text-sm font-semibold">
|
||||
<h3 className="dark:text-text-neutral-primary/90 text-sm font-semibold">
|
||||
Query Parameters
|
||||
</h3>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const QueryParametersForm = ({
|
||||
}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
aria-label={param.label}
|
||||
className="border-border-neutral-secondary bg-bg-neutral-primary text-text-primary focus:ring-primary dark:border-border-neutral-secondary dark:bg-bg-neutral-primary dark:text-text-primary h-4 w-4 rounded border focus:ring-2"
|
||||
className="border-border-neutral-secondary bg-bg-neutral-primary text-text-primary focus:ring-button-primary dark:border-border-neutral-secondary dark:bg-bg-neutral-primary dark:text-text-primary h-4 w-4 rounded border focus:ring-2"
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ vi.mock("@/components/shadcn/tooltip", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/entities/entity-info", () => ({
|
||||
vi.mock("@/components/shadcn/entities/entity-info", () => ({
|
||||
EntityInfo: ({
|
||||
entityAlias,
|
||||
entityId,
|
||||
@@ -47,11 +47,11 @@ vi.mock("@/components/ui/entities/entity-info", () => ({
|
||||
}) => <div>{entityAlias ?? entityId}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/entities/date-with-time", () => ({
|
||||
vi.mock("@/components/shadcn/entities/date-with-time", () => ({
|
||||
DateWithTime: ({ dateTime }: { dateTime: string }) => <span>{dateTime}</span>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/table", () => ({
|
||||
vi.mock("@/components/shadcn/table", () => ({
|
||||
DataTableColumnHeader: ({ title }: { title: string }) => <span>{title}</span>,
|
||||
DataTable: ({
|
||||
columns,
|
||||
|
||||
+3
-3
@@ -5,18 +5,18 @@ import { Check, Minus } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { DateWithTime } from "@/components/shadcn/entities/date-with-time";
|
||||
import { EntityInfo } from "@/components/shadcn/entities/entity-info";
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/components/shadcn/radio-group/radio-group";
|
||||
import { DataTable, DataTableColumnHeader } from "@/components/shadcn/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import { EntityInfo } from "@/components/ui/entities/entity-info";
|
||||
import { DataTable, DataTableColumnHeader } from "@/components/ui/table";
|
||||
import { formatDuration } from "@/lib/date-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { MetaDataProps, ProviderType } from "@/types";
|
||||
|
||||
+25
@@ -101,6 +101,31 @@ describe("waiting states", () => {
|
||||
});
|
||||
|
||||
describe("running a query", () => {
|
||||
test("the query builder surface uses the shared card primitive", async ({
|
||||
mountWith,
|
||||
}) => {
|
||||
const graph = await mountWith();
|
||||
|
||||
const card = await graph.waitFor(() => graph.queryBuilderCard, 10000);
|
||||
|
||||
expect(card).toHaveAttribute("data-slot", "card");
|
||||
expect(card).toHaveClass("rounded-xl");
|
||||
});
|
||||
|
||||
test("a parameterized query shows its required inputs after selection", async ({
|
||||
mountWith,
|
||||
}) => {
|
||||
const graph = await mountWith(fixtures.parameterizedQuery());
|
||||
|
||||
await graph.selectQuery();
|
||||
|
||||
expect(graph.containsText(/Query Parameters/i)).toBe(true);
|
||||
expect(graph.containsText(/Tag key/i)).toBe(true);
|
||||
expect(graph.getInputByName("tag_key")).toBeTruthy();
|
||||
expect(graph.containsText(/Tag value/i)).toBe(true);
|
||||
expect(graph.getInputByName("tag_value")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("the graph renders with a background, a minimap, and a viewport", async ({
|
||||
mountWith,
|
||||
}) => {
|
||||
|
||||
@@ -242,6 +242,40 @@ export const resourcesOnly = (): PageFixture => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const parameterizedQuery = (): PageFixture => ({
|
||||
scans: [buildScan(TYPICAL_SCAN_ID)],
|
||||
scanId: TYPICAL_SCAN_ID,
|
||||
queries: [
|
||||
buildQuery(
|
||||
"aws-internet-exposed-ec2-sensitive-s3-access",
|
||||
"Internet-Exposed EC2 with Sensitive S3 Access",
|
||||
{
|
||||
parameters: [
|
||||
{
|
||||
name: "tag_key",
|
||||
label: "Tag key",
|
||||
data_type: "string",
|
||||
description: "Tag key to filter the S3 bucket.",
|
||||
placeholder: "DataClassification",
|
||||
},
|
||||
{
|
||||
name: "tag_value",
|
||||
label: "Tag value",
|
||||
data_type: "string",
|
||||
description: "Tag value to filter the S3 bucket.",
|
||||
placeholder: "Sensitive",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
queryId: "aws-internet-exposed-ec2-sensitive-s3-access",
|
||||
queryResult: {
|
||||
nodes: [buildResourceNode("s3-1", "S3Bucket", "sensitive-data")],
|
||||
relationships: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const disconnected = (): PageFixture => ({
|
||||
scans: [buildScan(TYPICAL_SCAN_ID)],
|
||||
scanId: TYPICAL_SCAN_ID,
|
||||
@@ -322,6 +356,7 @@ export const fixtures = {
|
||||
singleNode,
|
||||
findingsOnly,
|
||||
resourcesOnly,
|
||||
parameterizedQuery,
|
||||
disconnected,
|
||||
large,
|
||||
edgeCases,
|
||||
|
||||
@@ -216,6 +216,22 @@ export class AttackPathPageHarness {
|
||||
return this.q(AttackPathPageHarness.VIEWPORT_SEL);
|
||||
}
|
||||
|
||||
get queryBuilderCard(): HTMLElement | null {
|
||||
return (
|
||||
this.container
|
||||
.querySelector<HTMLElement>(
|
||||
'[data-tour-id="attack-paths-query-selector"]',
|
||||
)
|
||||
?.closest<HTMLElement>('[data-slot="card"]') ?? null
|
||||
);
|
||||
}
|
||||
|
||||
getInputByName(name: string): HTMLInputElement | null {
|
||||
return this.container.querySelector<HTMLInputElement>(
|
||||
`input[name="${name}"]`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline `transform` of the React Flow viewport element. This is the
|
||||
* pan/zoom matrix React Flow rewrites on every fit/zoom/pan, so comparing
|
||||
|
||||
@@ -16,7 +16,7 @@ import { FindingDetailDrawer } from "@/components/findings/table";
|
||||
import { PageReady } from "@/components/onboarding";
|
||||
import { useFindingDetails } from "@/components/resources/table/use-finding-details";
|
||||
import { AutoRefresh } from "@/components/scans";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { Button, Card, useToast } from "@/components/shadcn";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/shadcn/dialog";
|
||||
import { StatusAlert } from "@/components/shared/status-alert";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { useMountEffect } from "@/hooks/use-mount-effect";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import {
|
||||
@@ -70,7 +69,7 @@ import {
|
||||
} from "./_lib/get-attack-paths-view-state";
|
||||
|
||||
const SCROLL_CONTAINER_CLASS =
|
||||
"minimal-scrollbar rounded-large shadow-small border-border-neutral-secondary bg-bg-neutral-secondary relative z-0 flex w-full flex-col gap-4 overflow-auto border p-4";
|
||||
"minimal-scrollbar relative z-0 w-full gap-4 overflow-auto shadow-sm";
|
||||
|
||||
export default function AttackPathsPage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -427,9 +426,9 @@ export default function AttackPathsPage() {
|
||||
</div>
|
||||
|
||||
{viewState === ATTACK_PATHS_VIEW_STATES.LOADING ? (
|
||||
<div className={SCROLL_CONTAINER_CLASS}>
|
||||
<Card variant="base" className={SCROLL_CONTAINER_CLASS}>
|
||||
<p className="text-sm">Loading scans...</p>
|
||||
</div>
|
||||
</Card>
|
||||
) : viewState === ATTACK_PATHS_VIEW_STATES.NO_SCANS ? (
|
||||
// Keep the empty-scans tour anchor: attackPathsEmptyTour targets
|
||||
// data-tour-id="attack-paths-empty-scans-cta". The panel's NO_SCANS
|
||||
@@ -463,7 +462,7 @@ export default function AttackPathsPage() {
|
||||
)}
|
||||
|
||||
{scanId && (
|
||||
<div className={SCROLL_CONTAINER_CLASS}>
|
||||
<Card variant="base" className={SCROLL_CONTAINER_CLASS}>
|
||||
{queriesLoading ? (
|
||||
<p className="text-sm">Loading queries...</p>
|
||||
) : queriesError ? (
|
||||
@@ -514,14 +513,14 @@ export default function AttackPathsPage() {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{(graphState.loading ||
|
||||
(graphState.data &&
|
||||
graphState.data.nodes &&
|
||||
graphState.data.nodes.length > 0)) && (
|
||||
<div className={SCROLL_CONTAINER_CLASS}>
|
||||
<Card variant="base" className={SCROLL_CONTAINER_CLASS}>
|
||||
{graphState.loading ? (
|
||||
<GraphLoading />
|
||||
) : graphState.data &&
|
||||
@@ -664,7 +663,7 @@ export default function AttackPathsPage() {
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{finding.findingDetails && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
export default function AttackPathsLayout({
|
||||
children,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import {
|
||||
@@ -25,7 +24,7 @@ import {
|
||||
TopFailedSectionsCardSkeleton,
|
||||
} from "@/components/compliance";
|
||||
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { getComplianceMapper } from "@/lib/compliance/compliance-mapper";
|
||||
import {
|
||||
getReportTypeForCompliance,
|
||||
@@ -350,7 +349,7 @@ const SSRComplianceContent = async ({
|
||||
{/* <SectionsFailureRateCard categories={categoryHeatmapData} /> */}
|
||||
</div>
|
||||
|
||||
<Spacer className="bg-border-neutral-primary h-1 w-full rounded-full" />
|
||||
<div className="bg-border-neutral-primary h-1 w-full rounded-full" />
|
||||
<ClientAccordionWrapper
|
||||
hideExpandButton={complianceId.includes("mitre_attack")}
|
||||
items={accordionItems}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ComplianceFilters } from "@/components/compliance/compliance-header/com
|
||||
import { ComplianceOverviewGrid } from "@/components/compliance/compliance-overview-grid";
|
||||
import { Alert, AlertDescription } from "@/components/shadcn/alert";
|
||||
import { Card, CardContent } from "@/components/shadcn/card/card";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { pickLatestCisPerProvider } from "@/lib/compliance/compliance-report-types";
|
||||
import {
|
||||
ExpandedScanData,
|
||||
@@ -274,7 +274,7 @@ const ComplianceOverviewPanel = ({
|
||||
<Card
|
||||
variant="base"
|
||||
padding="none"
|
||||
className="minimal-scrollbar shadow-small relative z-0 w-full gap-4 overflow-auto"
|
||||
className="minimal-scrollbar relative z-0 w-full gap-4 overflow-auto shadow-sm"
|
||||
>
|
||||
<CardContent className="flex flex-col gap-4 p-4">{children}</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { CloudIcon, FolderIcon, ServerIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { DataTable } from "@/components/ui/table/data-table";
|
||||
import { DataTableExpandAllToggle } from "@/components/ui/table/data-table-expand-all-toggle";
|
||||
import { DataTableExpandableCell } from "@/components/ui/table/data-table-expandable-cell";
|
||||
import { DataTable } from "@/components/shadcn/table/data-table";
|
||||
import { DataTableExpandAllToggle } from "@/components/shadcn/table/data-table-expand-all-toggle";
|
||||
import { DataTableExpandableCell } from "@/components/shadcn/table/data-table-expandable-cell";
|
||||
|
||||
/**
|
||||
* Demo page for the Expandable DataTable component.
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function DemoTreeViewPage() {
|
||||
<TooltipContent side="top">{item.name}</TooltipContent>
|
||||
</Tooltip>
|
||||
{hasChildren && !isLeaf && (
|
||||
<span className="bg-prowler-white/10 inline-flex min-w-5 shrink-0 items-center justify-center rounded px-1 py-0.5 text-xs tabular-nums">
|
||||
<span className="inline-flex min-w-5 shrink-0 items-center justify-center rounded bg-white/10 px-1 py-0.5 text-xs tabular-nums">
|
||||
{item.children?.length}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/shadcn/card/card";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
import { SentryErrorSource, SentryErrorType } from "@/sentry";
|
||||
|
||||
export default function Error({
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
FindingsGroupTable,
|
||||
SkeletonTableFindings,
|
||||
} from "@/components/findings/table";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { FilterTransitionWrapper } from "@/contexts";
|
||||
import {
|
||||
applyDefaultMutedFilter,
|
||||
@@ -166,7 +166,7 @@ const SSRDataTable = async ({
|
||||
return (
|
||||
<>
|
||||
{findingGroupsData?.errors?.length > 0 && (
|
||||
<div className="text-small mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-red-700">
|
||||
<div className="mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-sm text-red-700">
|
||||
<p className="mr-2 font-semibold">Error:</p>
|
||||
<p>{findingGroupsData.errors[0].detail}</p>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getIntegrations } from "@/actions/integrations";
|
||||
import { getAllProviders } from "@/actions/providers";
|
||||
import { S3IntegrationsManager } from "@/components/integrations/s3/s3-integrations-manager";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
interface S3IntegrationsProps {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getIntegrations } from "@/actions/integrations";
|
||||
import { getAllProviders } from "@/actions/providers";
|
||||
import { SecurityHubIntegrationsManager } from "@/components/integrations/security-hub/security-hub-integrations-manager";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
interface SecurityHubIntegrationsProps {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getIntegrations } from "@/actions/integrations";
|
||||
import { JiraIntegrationsManager } from "@/components/integrations/jira/jira-integrations-manager";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
interface JiraIntegrationsProps {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SecurityHubIntegrationCard,
|
||||
SsoLinkCard,
|
||||
} from "@/components/integrations";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
export default async function Integrations() {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import React from "react";
|
||||
|
||||
import { WorkflowSendInvite } from "@/components/invitations/workflow";
|
||||
import { NavigationHeader } from "@/components/ui";
|
||||
import { NavigationHeader } from "@/components/shadcn";
|
||||
|
||||
interface InvitationLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -18,7 +17,7 @@ export default function InvitationLayout({ children }: InvitationLayoutProps) {
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/invitations"
|
||||
/>
|
||||
<Spacer y={16} />
|
||||
<div className="h-16" />
|
||||
<div className="grid grid-cols-1 gap-8 px-4 lg:grid-cols-12 lg:px-0">
|
||||
<div className="order-1 my-auto h-full lg:col-span-4 lg:col-start-2">
|
||||
<WorkflowSendInvite />
|
||||
|
||||
@@ -3,16 +3,14 @@ import { Suspense } from "react";
|
||||
|
||||
import { getInvitations } from "@/actions/invitations/invitation";
|
||||
import { getRoles } from "@/actions/roles";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { filterInvitations } from "@/components/filters/data-filters";
|
||||
import { AddIcon } from "@/components/icons";
|
||||
import {
|
||||
ColumnsInvitation,
|
||||
SkeletonTableInvitation,
|
||||
} from "@/components/invitations/table";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/shadcn/table";
|
||||
import { InvitationProps, Role, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Invitations({
|
||||
@@ -25,17 +23,15 @@ export default async function Invitations({
|
||||
|
||||
return (
|
||||
<ContentLayout title="Invitations" icon="lucide:mail">
|
||||
<FilterControls search />
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-end justify-between">
|
||||
<DataTableFilterCustom filters={filterInvitations || []} />
|
||||
<DataTableFilterCustom
|
||||
filters={filterInvitations || []}
|
||||
gridClassName="w-fit grid-cols-[14rem_auto] items-center gap-4 sm:grid-cols-[14rem_auto] lg:grid-cols-[14rem_auto] xl:grid-cols-[14rem_auto] 2xl:grid-cols-[14rem_auto]"
|
||||
/>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/invitations/new">
|
||||
Send Invitation
|
||||
<AddIcon size={20} />
|
||||
</Link>
|
||||
<Link href="/invitations/new">Send Invitation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -124,6 +120,7 @@ const SSRDataTable = async ({
|
||||
columns={ColumnsInvitation}
|
||||
data={expandedResponse?.data || []}
|
||||
metadata={invitationsData?.meta}
|
||||
showSearch
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,16 +6,16 @@ import { ReactNode, Suspense } from "react";
|
||||
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { getScansByState } from "@/actions/scans/scans";
|
||||
import MainLayout from "@/components/layout/main-layout/main-layout";
|
||||
import {
|
||||
OnboardingCheckpointWatcher,
|
||||
OnboardingGate,
|
||||
OnboardingSequenceBanner,
|
||||
} from "@/components/onboarding";
|
||||
import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config";
|
||||
import MainLayout from "@/components/ui/main-layout/main-layout";
|
||||
import { NavigationProgress } from "@/components/ui/navigation-progress";
|
||||
import { Toaster } from "@/components/ui/toast";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import { NavigationProgress } from "@/components/shadcn/navigation-progress";
|
||||
import { Toaster } from "@/components/shadcn/toast";
|
||||
import { fontMono, fontSans } from "@/config/fonts";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -83,8 +83,9 @@ export default async function RootLayout({
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"bg-background min-h-screen font-sans antialiased",
|
||||
"bg-bg-neutral-primary min-h-screen font-sans antialiased",
|
||||
fontSans.variable,
|
||||
fontMono.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
|
||||
@@ -29,7 +29,8 @@ vi.mock("next/navigation", () => ({
|
||||
|
||||
// Action feedback is delivered through toasts (rendered by the layout Toaster),
|
||||
// so we assert the dispatched toast rather than in-page banner text.
|
||||
vi.mock("@/components/ui", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useToast: () => ({ toast: toastMock, dismiss: vi.fn() }),
|
||||
}));
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
type LighthouseV2ProviderType,
|
||||
type LighthouseV2SupportedProvider,
|
||||
} from "@/app/(prowler)/lighthouse/_types";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { Card } from "@/components/shadcn/card/card";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { useMountEffect } from "@/hooks/use-mount-effect";
|
||||
|
||||
import { LighthouseV2BusinessContextForm } from "./business-context-form";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { LighthouseV2NavigationModeSync } from "@/app/(prowler)/lighthouse/_comp
|
||||
import { loadLighthouseV2ConnectedModels } from "@/app/(prowler)/lighthouse/_lib/model-loading";
|
||||
import { LighthouseIcon } from "@/components/icons/Icons";
|
||||
import { Chat } from "@/components/lighthouse-v1";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
@@ -14,8 +13,8 @@ import {
|
||||
import { DeleteLLMProviderForm } from "@/components/lighthouse-v1/forms/delete-llm-provider-form";
|
||||
import { WorkflowConnectLLM } from "@/components/lighthouse-v1/workflow";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { NavigationHeader } from "@/components/shadcn";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { NavigationHeader } from "@/components/ui";
|
||||
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
|
||||
import type { LighthouseProvider } from "@/types/lighthouse-v1";
|
||||
|
||||
@@ -81,7 +80,7 @@ export default function ConnectLLMLayout({ children }: ConnectLLMLayoutProps) {
|
||||
icon="icon-park-outline:close-small"
|
||||
href={LIGHTHOUSE_ROUTE.SETTINGS}
|
||||
/>
|
||||
<Spacer y={8} />
|
||||
<div className="h-8" />
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-12">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowConnectLLM />
|
||||
@@ -114,7 +113,7 @@ export default function ConnectLLMLayout({ children }: ConnectLLMLayoutProps) {
|
||||
Delete Provider
|
||||
</Button>
|
||||
</div>
|
||||
<Spacer y={4} />
|
||||
<div className="h-4" />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
LighthouseSettings,
|
||||
LLMProvidersTable,
|
||||
} from "@/components/lighthouse-v1";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -2,7 +2,7 @@ import "@/styles/globals.css";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
interface ProviderLayoutProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Textarea } from "@heroui/input";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useActionState, useEffect, useState } from "react";
|
||||
|
||||
@@ -10,11 +9,18 @@ import {
|
||||
getMutedFindingsConfig,
|
||||
updateMutedFindingsConfig,
|
||||
} from "@/actions/processors";
|
||||
import { Button, Card, Skeleton } from "@/components/shadcn";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
FieldError,
|
||||
Skeleton,
|
||||
Textarea,
|
||||
} from "@/components/shadcn";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { fontMono } from "@/config/fonts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
convertToYaml,
|
||||
defaultMutedFindingsConfig,
|
||||
@@ -132,6 +138,13 @@ export function AdvancedMutelistForm() {
|
||||
}
|
||||
};
|
||||
|
||||
const isConfigInvalid =
|
||||
(!hasUserStartedTyping && !!state?.errors?.configuration) ||
|
||||
!yamlValidation.isValid;
|
||||
const configErrorMessage =
|
||||
(!hasUserStartedTyping && state?.errors?.configuration) ||
|
||||
(!yamlValidation.isValid ? yamlValidation.error : "");
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card variant="base" className="p-6">
|
||||
@@ -159,7 +172,7 @@ export function AdvancedMutelistForm() {
|
||||
size="md"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-default-600 text-sm">
|
||||
<p className="text-text-neutral-secondary text-sm">
|
||||
Are you sure you want to delete this configuration? This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
@@ -193,10 +206,10 @@ export function AdvancedMutelistForm() {
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<h3 className="text-default-700 mb-2 text-lg font-semibold">
|
||||
<h3 className="text-text-neutral-secondary mb-2 text-lg font-semibold">
|
||||
Advanced Mutelist Configuration
|
||||
</h3>
|
||||
<ul className="text-default-600 mb-4 list-disc pl-5 text-sm">
|
||||
<ul className="text-text-neutral-secondary mb-4 list-disc pl-5 text-sm">
|
||||
<li>
|
||||
<strong>
|
||||
This Mutelist configuration will take effect on the next
|
||||
@@ -227,7 +240,7 @@ export function AdvancedMutelistForm() {
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="configuration"
|
||||
className="text-default-700 text-sm font-medium"
|
||||
className="text-text-neutral-secondary text-sm font-medium"
|
||||
>
|
||||
Mutelist Configuration (YAML)
|
||||
</label>
|
||||
@@ -236,29 +249,26 @@ export function AdvancedMutelistForm() {
|
||||
id="configuration"
|
||||
name="configuration"
|
||||
placeholder={defaultMutedFindingsConfig}
|
||||
variant="bordered"
|
||||
value={configText}
|
||||
onChange={(e) => handleConfigChange(e.target.value)}
|
||||
minRows={20}
|
||||
maxRows={20}
|
||||
isInvalid={
|
||||
(!hasUserStartedTyping && !!state?.errors?.configuration) ||
|
||||
!yamlValidation.isValid
|
||||
}
|
||||
errorMessage={
|
||||
(!hasUserStartedTyping && state?.errors?.configuration) ||
|
||||
(!yamlValidation.isValid ? yamlValidation.error : "")
|
||||
}
|
||||
classNames={{
|
||||
input: fontMono.className + " text-sm",
|
||||
base: "min-h-[400px]",
|
||||
errorMessage: "whitespace-pre-wrap",
|
||||
}}
|
||||
rows={20}
|
||||
aria-invalid={isConfigInvalid}
|
||||
className={cn(
|
||||
fontMono.className,
|
||||
"min-h-[400px] text-sm",
|
||||
isConfigInvalid &&
|
||||
"border-border-error focus:border-border-error focus:ring-border-error",
|
||||
)}
|
||||
/>
|
||||
{isConfigInvalid && configErrorMessage && (
|
||||
<FieldError className="my-1 px-1 whitespace-pre-wrap">
|
||||
{configErrorMessage}
|
||||
</FieldError>
|
||||
)}
|
||||
{yamlValidation.isValid &&
|
||||
configText &&
|
||||
hasUserStartedTyping && (
|
||||
<div className="text-tiny text-success my-1 flex items-center px-1">
|
||||
<div className="text-text-success-primary my-1 flex items-center px-1 text-xs">
|
||||
<span>Valid YAML format</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,13 +11,7 @@ vi.mock("@/actions/mute-rules", () => ({
|
||||
updateMuteRule: updateMuteRuleMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui", () => ({
|
||||
useToast: () => ({
|
||||
toast: toastMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/form", () => ({
|
||||
vi.mock("@/components/shadcn/form", () => ({
|
||||
FormButtons: ({
|
||||
onCancel,
|
||||
submitText = "Save",
|
||||
@@ -38,7 +32,9 @@ vi.mock("@/components/ui/form", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useToast: () => ({ toast: toastMock }),
|
||||
Input: ({
|
||||
defaultValue,
|
||||
...props
|
||||
@@ -53,7 +49,7 @@ vi.mock("@/components/shadcn", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/form/Label", () => ({
|
||||
vi.mock("@/components/shadcn/form/Label", () => ({
|
||||
Label: ({
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -5,8 +5,8 @@ import { FormEvent, useState } from "react";
|
||||
import { updateMuteRule } from "@/actions/mute-rules";
|
||||
import { MuteRuleActionState, MuteRuleData } from "@/actions/mute-rules/types";
|
||||
import { Input, Textarea } from "@/components/shadcn";
|
||||
import { FormButtons } from "@/components/ui/form";
|
||||
import { Label } from "@/components/ui/form/Label";
|
||||
import { FormButtons } from "@/components/shadcn/form";
|
||||
import { Label } from "@/components/shadcn/form/Label";
|
||||
import { useMuteRuleAction } from "@/hooks/use-mute-rule-action";
|
||||
import {
|
||||
enforceMuteRuleReasonLimit,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { Switch } from "@heroui/switch";
|
||||
import { useState } from "react";
|
||||
|
||||
import { toggleMuteRule } from "@/actions/mute-rules";
|
||||
import { MuteRuleData } from "@/actions/mute-rules/types";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { Switch } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
|
||||
interface MuteRuleEnabledToggleProps {
|
||||
muteRule: MuteRuleData;
|
||||
@@ -44,10 +44,9 @@ export function MuteRuleEnabledToggle({
|
||||
|
||||
return (
|
||||
<Switch
|
||||
isSelected={isEnabled}
|
||||
onValueChange={handleToggle}
|
||||
isDisabled={isLoading}
|
||||
size="sm"
|
||||
checked={isEnabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isLoading}
|
||||
aria-label={`Toggle mute rule ${muteRule.attributes.name}`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { type ReactNode } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { type MuteRuleTableData } from "./mute-rule-target-previews";
|
||||
|
||||
vi.mock("@/components/shadcn/modal", () => ({
|
||||
Modal: ({
|
||||
children,
|
||||
open,
|
||||
title,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
open: boolean;
|
||||
title?: string;
|
||||
}) =>
|
||||
open ? (
|
||||
<div role="dialog" aria-label={title}>
|
||||
{children}
|
||||
</div>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
import { MuteRuleTargetsModal } from "./mute-rule-targets-modal";
|
||||
|
||||
const longMuteRule: MuteRuleTableData = {
|
||||
type: "mute-rules",
|
||||
id: "mute-rule-1",
|
||||
attributes: {
|
||||
inserted_at: "2026-04-22T09:00:00Z",
|
||||
updated_at: "2026-04-22T09:05:00Z",
|
||||
name: "Finding triage: Risk Accepted - 019f1d6f-b304-78a6-8a59-9f648f65d123",
|
||||
reason: "Existing reason",
|
||||
enabled: true,
|
||||
finding_uids: ["uid-1"],
|
||||
},
|
||||
targetLabels: [
|
||||
"Security Hub is enabled with standards or integrations configured • hub/unknown",
|
||||
],
|
||||
targetSummaryLabel:
|
||||
"Security Hub is enabled with standards or integrations configured • hub/unknown",
|
||||
hiddenTargetCount: 0,
|
||||
};
|
||||
|
||||
describe("MuteRuleTargetsModal", () => {
|
||||
it("keeps long mute rule and finding cards constrained to the modal width", () => {
|
||||
// Given / When
|
||||
render(
|
||||
<MuteRuleTargetsModal
|
||||
muteRule={longMuteRule}
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Then
|
||||
const dialog = screen.getByRole("dialog", { name: "Muted Findings" });
|
||||
const content = dialog.firstElementChild;
|
||||
if (!(content instanceof HTMLElement)) {
|
||||
throw new Error("Expected modal content wrapper");
|
||||
}
|
||||
|
||||
const muteRuleCard = within(dialog)
|
||||
.getByText("Mute rule")
|
||||
.closest("div")?.parentElement;
|
||||
if (!(muteRuleCard instanceof HTMLElement)) {
|
||||
throw new Error("Expected mute rule card");
|
||||
}
|
||||
|
||||
const targetList = within(dialog).getByRole("list");
|
||||
const targetsCard = targetList.parentElement;
|
||||
if (!(targetsCard instanceof HTMLElement)) {
|
||||
throw new Error("Expected targets card");
|
||||
}
|
||||
|
||||
expect(content).toHaveClass("min-w-0", "max-w-full");
|
||||
expect(muteRuleCard).toHaveClass(
|
||||
"min-w-0",
|
||||
"max-w-full",
|
||||
"overflow-hidden",
|
||||
);
|
||||
expect(targetsCard).toHaveClass("min-w-0", "max-w-full", "overflow-hidden");
|
||||
});
|
||||
});
|
||||
@@ -29,9 +29,9 @@ export function MuteRuleTargetsModal({
|
||||
description="Review every finding currently muted by this rule."
|
||||
size="xl"
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="border-border-neutral-secondary bg-bg-neutral-tertiary flex items-start justify-between gap-4 rounded-xl border p-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex max-w-full min-w-0 flex-col gap-5">
|
||||
<div className="border-border-neutral-secondary bg-bg-neutral-tertiary flex max-w-full min-w-0 items-start justify-between gap-4 overflow-hidden rounded-xl border p-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-text-neutral-tertiary text-xs font-medium tracking-[0.08em] uppercase">
|
||||
Mute rule
|
||||
</p>
|
||||
@@ -48,7 +48,7 @@ export function MuteRuleTargetsModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="minimal-scrollbar border-border-neutral-secondary bg-bg-neutral-tertiary max-h-[60vh] overflow-y-auto rounded-xl border">
|
||||
<div className="minimal-scrollbar border-border-neutral-secondary bg-bg-neutral-tertiary max-h-[60vh] max-w-full min-w-0 overflow-hidden overflow-y-auto rounded-xl border">
|
||||
<ul className="divide-border-neutral-secondary divide-y">
|
||||
{muteRule.targetLabels.map((label, index) => {
|
||||
const [title, ...metaParts] = label.split(" • ");
|
||||
@@ -57,7 +57,7 @@ export function MuteRuleTargetsModal({
|
||||
return (
|
||||
<li
|
||||
key={`${muteRule.id}-${label}-${index}`}
|
||||
className="min-w-0 px-4 py-3"
|
||||
className="max-w-full min-w-0 px-4 py-3"
|
||||
>
|
||||
<p className="text-text-neutral-primary text-sm font-medium break-all">
|
||||
{title}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createMuteRulesColumns } from "./mute-rules-columns";
|
||||
|
||||
vi.mock("@/components/ui/entities", () => ({
|
||||
vi.mock("@/components/shadcn/entities", () => ({
|
||||
DateWithTime: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/table", () => ({
|
||||
vi.mock("@/components/shadcn/table", () => ({
|
||||
DataTableColumnHeader: ({ title }: { title: string }) => <span>{title}</span>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { List } from "lucide-react";
|
||||
|
||||
import { MuteRuleData } from "@/actions/mute-rules/types";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import { DataTableColumnHeader } from "@/components/ui/table";
|
||||
import { DateWithTime } from "@/components/shadcn/entities";
|
||||
import { DataTableColumnHeader } from "@/components/shadcn/table";
|
||||
|
||||
import { MuteRuleEnabledToggle } from "./mute-rule-enabled-toggle";
|
||||
import { MuteRuleRowActions } from "./mute-rule-row-actions";
|
||||
|
||||
@@ -19,13 +19,9 @@ vi.mock("@/actions/mute-rules", () => ({
|
||||
deleteMuteRule: deleteMuteRuleMock,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui", () => ({
|
||||
useToast: () => ({
|
||||
toast: toastMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn", () => ({
|
||||
vi.mock("@/components/shadcn", async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
useToast: () => ({ toast: toastMock }),
|
||||
Button: ({
|
||||
children,
|
||||
...props
|
||||
@@ -52,7 +48,7 @@ vi.mock("@/components/shadcn/modal", () => ({
|
||||
) : null,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/table", () => ({
|
||||
vi.mock("@/components/shadcn/table", () => ({
|
||||
DataTable: (props: {
|
||||
columns: Array<{
|
||||
id?: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useDisclosure } from "@heroui/use-disclosure";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FormEvent, useState } from "react";
|
||||
@@ -8,9 +7,9 @@ import { FormEvent, useState } from "react";
|
||||
import { deleteMuteRule } from "@/actions/mute-rules";
|
||||
import { MuteRuleData } from "@/actions/mute-rules/types";
|
||||
import { CardTitle } from "@/components/shadcn";
|
||||
import { FormButtons } from "@/components/shadcn/form";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { FormButtons } from "@/components/ui/form";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import { useMuteRuleAction } from "@/hooks/use-mute-rule-action";
|
||||
import { MetaDataProps } from "@/types";
|
||||
|
||||
@@ -37,27 +36,27 @@ export function MuteRulesTableClient({
|
||||
const { isPending: isDeleting, runAction: runDeleteAction } =
|
||||
useMuteRuleAction();
|
||||
|
||||
const editModal = useDisclosure();
|
||||
const deleteModal = useDisclosure();
|
||||
const targetsModal = useDisclosure();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isTargetsModalOpen, setIsTargetsModalOpen] = useState(false);
|
||||
|
||||
const handleEditClick = (muteRule: MuteRuleData) => {
|
||||
setSelectedMuteRule(muteRule);
|
||||
editModal.onOpen();
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (muteRule: MuteRuleData) => {
|
||||
setSelectedMuteRule(muteRule);
|
||||
deleteModal.onOpen();
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const handleViewTargets = (muteRule: MuteRuleTableData) => {
|
||||
setSelectedTargetsRule(muteRule);
|
||||
targetsModal.onOpen();
|
||||
setIsTargetsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditSuccess = () => {
|
||||
editModal.onClose();
|
||||
setIsEditModalOpen(false);
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
@@ -68,7 +67,7 @@ export function MuteRulesTableClient({
|
||||
|
||||
runDeleteAction(() => deleteMuteRule(null, formData), {
|
||||
onSuccess: () => {
|
||||
deleteModal.onClose();
|
||||
setIsDeleteModalOpen(false);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
@@ -102,15 +101,15 @@ export function MuteRulesTableClient({
|
||||
|
||||
<MuteRuleTargetsModal
|
||||
muteRule={selectedTargetsRule}
|
||||
open={targetsModal.isOpen}
|
||||
onOpenChange={targetsModal.onOpenChange}
|
||||
open={isTargetsModalOpen}
|
||||
onOpenChange={setIsTargetsModalOpen}
|
||||
/>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{selectedMuteRule && (
|
||||
<Modal
|
||||
open={editModal.isOpen}
|
||||
onOpenChange={editModal.onOpenChange}
|
||||
open={isEditModalOpen}
|
||||
onOpenChange={setIsEditModalOpen}
|
||||
title="Edit Mute Rule"
|
||||
description="Update the rule metadata without changing the muted findings linked to it."
|
||||
size="lg"
|
||||
@@ -119,7 +118,7 @@ export function MuteRulesTableClient({
|
||||
key={selectedMuteRule.id}
|
||||
muteRule={selectedMuteRule}
|
||||
onSuccess={handleEditSuccess}
|
||||
onCancel={editModal.onClose}
|
||||
onCancel={() => setIsEditModalOpen(false)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
@@ -127,8 +126,8 @@ export function MuteRulesTableClient({
|
||||
{/* Delete Confirmation Modal */}
|
||||
{selectedMuteRule && (
|
||||
<Modal
|
||||
open={deleteModal.isOpen}
|
||||
onOpenChange={deleteModal.onOpenChange}
|
||||
open={isDeleteModalOpen}
|
||||
onOpenChange={setIsDeleteModalOpen}
|
||||
title="Delete Mute Rule"
|
||||
description="Remove this rule from Mutelist. Existing muted findings will remain muted."
|
||||
size="md"
|
||||
@@ -153,7 +152,7 @@ export function MuteRulesTableClient({
|
||||
</div>
|
||||
|
||||
<FormButtons
|
||||
onCancel={deleteModal.onClose}
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
submitText={isDeleting ? "Deleting..." : "Delete"}
|
||||
isDisabled={isDeleting}
|
||||
submitColor="danger"
|
||||
|
||||
@@ -43,8 +43,10 @@ export async function MuteRulesTable({ searchParams }: MuteRulesTableProps) {
|
||||
</h3>
|
||||
<p className="text-text-neutral-secondary mt-1 text-sm">
|
||||
Mute rules are created when you mute findings from the Findings
|
||||
page. Select findings and click "Mute" to create your
|
||||
first rule.
|
||||
page.
|
||||
<br />
|
||||
Select findings and click "Mute" to create your first
|
||||
rule.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,7 +96,7 @@ export function MuteRulesTableSkeleton() {
|
||||
return (
|
||||
<div
|
||||
data-testid="mute-rules-table-skeleton"
|
||||
className="rounded-large shadow-small border-border-neutral-secondary bg-bg-neutral-secondary flex w-full flex-col gap-4 overflow-hidden border p-4"
|
||||
className="border-border-neutral-secondary bg-bg-neutral-secondary flex w-full flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm"
|
||||
>
|
||||
<div
|
||||
data-testid="mute-rules-table-skeleton-intro"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { SearchParamsProps } from "@/types/components";
|
||||
|
||||
import { MuteRulesTable, MuteRulesTableSkeleton } from "./_components/simple";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getAllProviders } from "@/actions/providers";
|
||||
import { getLighthouseV2Configurations } from "@/app/(prowler)/lighthouse/_actions";
|
||||
import { ProviderAccountSelectors } from "@/components/filters/provider-account-selectors";
|
||||
import { ProviderGroupSelector } from "@/components/filters/provider-group-selector";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("profile page layout", () => {
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const pagePath = path.join(currentDir, "page.tsx");
|
||||
const source = readFileSync(pagePath, "utf8");
|
||||
|
||||
it("uses one large card with profile sections stacked vertically", () => {
|
||||
expect(source).toContain('aria-label="User profile settings"');
|
||||
expect(source).toContain('className="w-full gap-4 p-4 md:p-5"');
|
||||
expect(source).not.toContain("xl:grid-cols");
|
||||
expect(source).not.toContain('className="flex w-full flex-col gap-6"');
|
||||
|
||||
const sectionOrder = [
|
||||
"<UserBasicInfoCard",
|
||||
"<RolesCard",
|
||||
"<SamlIntegrationCard",
|
||||
"<MembershipsCard",
|
||||
"<ApiKeysCard",
|
||||
];
|
||||
|
||||
const sectionIndexes = sectionOrder.map((section) =>
|
||||
source.indexOf(section),
|
||||
);
|
||||
|
||||
expect(sectionIndexes).not.toContain(-1);
|
||||
expect(sectionIndexes).toEqual([...sectionIndexes].sort((a, b) => a - b));
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,8 @@ import { getSamlConfig } from "@/actions/integrations/saml";
|
||||
import { getUserInfo } from "@/actions/users/users";
|
||||
import { auth } from "@/auth.config";
|
||||
import { SamlIntegrationCard } from "@/components/integrations/saml/saml-integration-card";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { Card } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { ApiKeysCard, UserBasicInfoCard } from "@/components/users/profile";
|
||||
import { MembershipsCard } from "@/components/users/profile/memberships-card";
|
||||
import { RolesCard } from "@/components/users/profile/roles-card";
|
||||
@@ -96,25 +97,25 @@ const SSRDataUser = async ({
|
||||
const samlConfig = hasManageIntegrations ? await getSamlConfig() : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<Card
|
||||
variant="base"
|
||||
padding="none"
|
||||
role="region"
|
||||
aria-label="User profile settings"
|
||||
className="w-full gap-4 p-4 md:p-5"
|
||||
>
|
||||
<UserBasicInfoCard user={userData} tenantId={userTenantId || ""} />
|
||||
<div className="flex flex-col gap-6 xl:flex-row">
|
||||
<div className="flex w-full flex-col gap-6 xl:max-w-[50%]">
|
||||
<RolesCard roles={roleDetails} roleDetails={roleDetailsMap} />
|
||||
{hasManageIntegrations && (
|
||||
<SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-6 xl:max-w-[50%]">
|
||||
<MembershipsCard
|
||||
memberships={membershipsIncluded}
|
||||
tenantsMap={tenantsMap}
|
||||
hasManageAccount={hasManageAccount}
|
||||
sessionTenantId={session?.tenantId}
|
||||
/>
|
||||
{hasManageAccount && <ApiKeysCard searchParams={searchParams} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RolesCard roles={roleDetails} roleDetails={roleDetailsMap} />
|
||||
{hasManageIntegrations && (
|
||||
<SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} />
|
||||
)}
|
||||
<MembershipsCard
|
||||
memberships={membershipsIncluded}
|
||||
tenantsMap={tenantsMap}
|
||||
hasManageAccount={hasManageAccount}
|
||||
sessionTenantId={session?.tenantId}
|
||||
/>
|
||||
{hasManageAccount && <ApiKeysCard searchParams={searchParams} />}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import { listScanConfigurations } from "@/actions/scan-configurations";
|
||||
import { ProvidersAccountsView } from "@/components/providers";
|
||||
import { SkeletonTableProviders } from "@/components/providers/table";
|
||||
import { CliImportBanner } from "@/components/scans";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { FilterTransitionWrapper } from "@/contexts";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
import {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getAllProviders } from "@/actions/providers";
|
||||
import { getRoles } from "@/actions/roles";
|
||||
import { AddGroupForm, EditGroupForm } from "@/components/manage-groups/forms";
|
||||
import { ColumnGroups } from "@/components/manage-groups/table";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import { ProviderProps, Role, SearchParamsProps } from "@/types";
|
||||
|
||||
export const ProviderGroupsContent = async ({
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { ResourcesFilters } from "@/components/resources/resources-filters";
|
||||
import { SkeletonTableResources } from "@/components/resources/skeleton/skeleton-table-resources";
|
||||
import { ResourcesTableWithSelection } from "@/components/resources/table";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { FilterTransitionWrapper } from "@/contexts";
|
||||
import {
|
||||
createDict,
|
||||
@@ -172,7 +172,7 @@ const SSRDataTable = async ({
|
||||
return (
|
||||
<>
|
||||
{resourcesData?.errors && (
|
||||
<div className="text-small mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-red-700">
|
||||
<div className="mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-sm text-red-700">
|
||||
<p className="mr-2 font-semibold">Error:</p>
|
||||
<p>{resourcesData.errors[0].detail}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
import React from "react";
|
||||
|
||||
import { WorkflowAddEditRole } from "@/components/roles/workflow";
|
||||
import { NavigationHeader } from "@/components/ui";
|
||||
import { NavigationHeader } from "@/components/shadcn";
|
||||
|
||||
interface RoleLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -18,7 +17,7 @@ export default function RoleLayout({ children }: RoleLayoutProps) {
|
||||
icon="icon-park-outline:close-small"
|
||||
href="/roles"
|
||||
/>
|
||||
<Spacer y={16} />
|
||||
<div className="h-16" />
|
||||
<div className="grid grid-cols-1 gap-8 px-4 sm:px-6 lg:grid-cols-12 lg:px-0">
|
||||
<div className="order-1 my-auto hidden h-full lg:col-span-4 lg:col-start-2 lg:block">
|
||||
<WorkflowAddEditRole />
|
||||
|
||||
@@ -2,13 +2,11 @@ import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getRoles } from "@/actions/roles";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { filterRoles } from "@/components/filters/data-filters";
|
||||
import { AddIcon } from "@/components/icons";
|
||||
import { ColumnsRoles, SkeletonTableRoles } from "@/components/roles/table";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/shadcn/table";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Roles({
|
||||
@@ -21,16 +19,14 @@ export default async function Roles({
|
||||
|
||||
return (
|
||||
<ContentLayout title="Roles" icon="lucide:user-cog">
|
||||
<FilterControls search />
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-end justify-between">
|
||||
<DataTableFilterCustom filters={filterRoles || []} />
|
||||
<DataTableFilterCustom
|
||||
filters={filterRoles || []}
|
||||
gridClassName="w-fit grid-cols-[14rem_auto] items-center gap-4 sm:grid-cols-[14rem_auto] lg:grid-cols-[14rem_auto] xl:grid-cols-[14rem_auto] 2xl:grid-cols-[14rem_auto]"
|
||||
/>
|
||||
<Button asChild>
|
||||
<Link href="/roles/new">
|
||||
Add Role
|
||||
<AddIcon size={20} />
|
||||
</Link>
|
||||
<Link href="/roles/new">Add Role</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +63,7 @@ const SSRDataTable = async ({
|
||||
columns={ColumnsRoles}
|
||||
data={rolesData?.data || []}
|
||||
metadata={rolesData?.meta}
|
||||
showSearch
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
Input,
|
||||
Textarea,
|
||||
} from "@/components/shadcn";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { DOCS_URLS } from "@/lib/external-urls";
|
||||
import {
|
||||
convertToYaml,
|
||||
@@ -190,10 +190,10 @@ function ScanConfigurationForm({
|
||||
<FieldLabel htmlFor="scan-configuration-yaml">
|
||||
Configuration (YAML)
|
||||
</FieldLabel>
|
||||
<ul className="text-default-500 text-tiny mb-1 list-disc pl-5">
|
||||
<ul className="text-text-neutral-tertiary mb-1 list-disc pl-5 text-xs">
|
||||
<li>
|
||||
Follows the structure of{" "}
|
||||
<code className="bg-default-100 text-default-600 rounded px-1 py-0.5 font-mono">
|
||||
<code className="bg-bg-neutral-tertiary text-text-neutral-secondary rounded px-1 py-0.5 font-mono">
|
||||
prowler/config/config.yaml
|
||||
</code>
|
||||
; include only the keys you want to override. Learn more{" "}
|
||||
@@ -225,7 +225,7 @@ function ScanConfigurationForm({
|
||||
{!yamlSyntax.isValid ? (
|
||||
<FieldError>{`Invalid YAML format: ${yamlSyntax.error}`}</FieldError>
|
||||
) : configText.trim() && !configError ? (
|
||||
<p className="text-tiny text-text-success-primary">
|
||||
<p className="text-text-success-primary text-xs">
|
||||
Valid YAML format
|
||||
</p>
|
||||
) : null}
|
||||
@@ -235,7 +235,7 @@ function ScanConfigurationForm({
|
||||
|
||||
<Field>
|
||||
<FieldLabel>Attach to providers (optional)</FieldLabel>
|
||||
<p className="text-default-500 text-tiny">
|
||||
<p className="text-text-neutral-tertiary text-xs">
|
||||
Pick the providers that should use this configuration on their next
|
||||
scan. You can save it without any and attach providers later — it just
|
||||
won't apply to a scan until one is attached.
|
||||
@@ -249,7 +249,7 @@ function ScanConfigurationForm({
|
||||
)}
|
||||
</p>
|
||||
{selectableProviders.length === 0 ? (
|
||||
<p className="text-default-500 text-tiny italic">
|
||||
<p className="text-text-neutral-tertiary text-xs italic">
|
||||
{richProviders.length === 0
|
||||
? "No providers available in this tenant."
|
||||
: "All providers are already attached to other Scan Configurations."}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
ActionDropdownDangerZone,
|
||||
ActionDropdownItem,
|
||||
} from "@/components/shadcn/dropdown";
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import { DataTableColumnHeader } from "@/components/ui/table";
|
||||
import { DateWithTime } from "@/components/shadcn/entities";
|
||||
import { DataTableColumnHeader } from "@/components/shadcn/table";
|
||||
import { ScanConfigurationData } from "@/types/scan-configurations";
|
||||
|
||||
export const createScanConfigurationsColumns = (
|
||||
|
||||
@@ -11,10 +11,10 @@ import { AccountsSelector } from "@/app/(prowler)/_overview/_components/accounts
|
||||
import { BatchFiltersLayout } from "@/components/filters/batch-filters-layout";
|
||||
import { ClearFiltersButton } from "@/components/filters/clear-filters-button";
|
||||
import { Button, Card } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
import { Modal } from "@/components/shadcn/modal";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import { DOCS_URLS } from "@/lib/external-urls";
|
||||
import { ProviderProps } from "@/types/providers";
|
||||
import { ScanConfigurationData } from "@/types/scan-configurations";
|
||||
@@ -199,12 +199,12 @@ export function ScanConfigurationsManager({
|
||||
{noMatchForProvider ? (
|
||||
<Card variant="base" padding="xl">
|
||||
<div className="text-center">
|
||||
<p className="text-default-700 text-sm font-medium">
|
||||
<p className="text-text-neutral-secondary text-sm font-medium">
|
||||
{providerFilter.length === 1
|
||||
? "No Scan Configuration is attached to this provider."
|
||||
: "No Scan Configuration is attached to any of the selected providers."}
|
||||
</p>
|
||||
<p className="text-default-500 mt-1 text-sm">
|
||||
<p className="text-text-neutral-tertiary mt-1 text-sm">
|
||||
The next scan{providerFilter.length === 1 ? "" : "s"} will use the
|
||||
built-in defaults shipped with Prowler. Attach a Scan
|
||||
Configuration from the editor to override them.
|
||||
@@ -241,7 +241,7 @@ export function ScanConfigurationsManager({
|
||||
size="md"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-default-600 text-sm">
|
||||
<p className="text-text-neutral-secondary text-sm">
|
||||
Are you sure you want to delete{" "}
|
||||
<strong>{pendingDelete?.attributes.name}</strong>? Attached
|
||||
providers will fall back to the built-in scan defaults on their next
|
||||
|
||||
@@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
|
||||
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { listScanConfigurations } from "@/actions/scan-configurations";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
|
||||
import { ScanConfigurationsManager } from "./_components/scan-configurations-manager";
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ScansPageShell } from "@/components/scans/scans-page-shell";
|
||||
import { ScansProvidersEmptyState } from "@/components/scans/scans-providers-empty-state";
|
||||
import { SkeletonTableScans } from "@/components/scans/table";
|
||||
import { ScanJobsTable } from "@/components/scans/table/scan-jobs-table";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import {
|
||||
buildProviderScheduleSummary,
|
||||
buildSchedulesByProviderId,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Spacer } from "@heroui/spacer";
|
||||
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
export default async function Services() {
|
||||
// const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
@@ -10,9 +8,9 @@ export default async function Services() {
|
||||
title="Services"
|
||||
icon="material-symbols:linked-services-outline"
|
||||
>
|
||||
<Spacer y={4} />
|
||||
<div className="h-4" />
|
||||
<FilterControls />
|
||||
<Spacer y={4} />
|
||||
<div className="h-4" />
|
||||
{/* <Suspense key={searchParamsKey} fallback={<ServiceSkeletonGrid />}>
|
||||
<SSRServiceGrid />
|
||||
</Suspense> */}
|
||||
|
||||
@@ -4,11 +4,9 @@ import { Suspense } from "react";
|
||||
import { getRoles } from "@/actions/roles/roles";
|
||||
import { getCurrentUserTenantRole, getUsers } from "@/actions/users/users";
|
||||
import { auth } from "@/auth.config";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import { AddIcon } from "@/components/icons";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import { ColumnsUser, SkeletonTableUser } from "@/components/users/table";
|
||||
import { Role, SearchParamsProps, UserProps } from "@/types";
|
||||
import { TENANT_MEMBERSHIP_ROLE } from "@/types/users";
|
||||
@@ -23,15 +21,10 @@ export default async function Users({
|
||||
|
||||
return (
|
||||
<ContentLayout title="Users" icon="lucide:user">
|
||||
<FilterControls search />
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-end justify-end">
|
||||
<Button asChild>
|
||||
<Link href="/invitations/new">
|
||||
Invite User
|
||||
<AddIcon size={20} />
|
||||
</Link>
|
||||
<Link href="/invitations/new">Invite User</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -121,6 +114,7 @@ const SSRDataTable = async ({
|
||||
columns={ColumnsUser}
|
||||
data={expandedUsers || []}
|
||||
metadata={usersData?.meta}
|
||||
showSearch
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { ContentLayout } from "@/components/shadcn/content-layout";
|
||||
|
||||
export default async function Workloads() {
|
||||
return (
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { HeroUIProvider } from "@heroui/system";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import * as React from "react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
}
|
||||
|
||||
export function Providers({ children, themeProps }: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<SessionProvider>
|
||||
<HeroUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</HeroUIProvider>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
UI components migrated from HeroUI to shared shadcn primitives
|
||||
@@ -1,96 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import type { SwitchProps } from "@heroui/switch";
|
||||
import { useSwitch } from "@heroui/switch";
|
||||
import { useIsSSR } from "@react-aria/ssr";
|
||||
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
||||
import clsx from "clsx";
|
||||
import { useTheme } from "next-themes";
|
||||
import { FC } from "react";
|
||||
import React from "react";
|
||||
import { ComponentProps, useSyncExternalStore } from "react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { MoonFilledIcon, SunFilledIcon } from "./icons";
|
||||
|
||||
export interface ThemeSwitchProps {
|
||||
className?: string;
|
||||
classNames?: SwitchProps["classNames"];
|
||||
}
|
||||
export type ThemeSwitchProps = ComponentProps<"button">;
|
||||
|
||||
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
||||
className,
|
||||
classNames,
|
||||
}) => {
|
||||
const emptySubscribe = () => () => {};
|
||||
|
||||
export function ThemeSwitch({ className, ...props }: ThemeSwitchProps) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const isSSR = useIsSSR();
|
||||
// Hydration-safe mounted check: false on the server, true after hydration
|
||||
const isHydrated = useSyncExternalStore(
|
||||
emptySubscribe,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
|
||||
const onChange = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
};
|
||||
|
||||
const {
|
||||
Component,
|
||||
slots,
|
||||
isSelected,
|
||||
getBaseProps,
|
||||
getInputProps,
|
||||
getWrapperProps,
|
||||
} = useSwitch({
|
||||
isSelected: theme === "light" || isSSR,
|
||||
"aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
|
||||
onChange,
|
||||
});
|
||||
const isLightMode = theme === "light" || !isHydrated;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Component
|
||||
{...getBaseProps({
|
||||
className: clsx(
|
||||
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
||||
className,
|
||||
classNames?.base,
|
||||
),
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
{...props}
|
||||
role="switch"
|
||||
aria-checked={isLightMode}
|
||||
aria-label={`Switch to ${isLightMode ? "dark" : "light"} mode`}
|
||||
onClick={() => setTheme(isLightMode ? "dark" : "light")}
|
||||
className={cn(
|
||||
"text-neutral-tertiary flex cursor-pointer items-center justify-center rounded-lg px-px pt-px transition-opacity hover:opacity-80",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<input {...getInputProps()} />
|
||||
</VisuallyHidden>
|
||||
<div
|
||||
{...getWrapperProps()}
|
||||
className={slots.wrapper({
|
||||
class: clsx(
|
||||
[
|
||||
"h-auto w-auto",
|
||||
"bg-transparent",
|
||||
"rounded-lg",
|
||||
"flex items-center justify-center",
|
||||
"group-data-[selected=true]:bg-transparent",
|
||||
"!text-default-500",
|
||||
"pt-px",
|
||||
"px-0",
|
||||
"mx-0",
|
||||
],
|
||||
classNames?.wrapper,
|
||||
),
|
||||
})}
|
||||
>
|
||||
{!isSelected || isSSR ? (
|
||||
<SunFilledIcon size={22} />
|
||||
) : (
|
||||
<MoonFilledIcon size={22} />
|
||||
)}
|
||||
</div>
|
||||
</Component>
|
||||
{isLightMode && isHydrated ? (
|
||||
<MoonFilledIcon size={22} />
|
||||
) : (
|
||||
<SunFilledIcon size={22} />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isSelected || isSSR ? "Switch to Dark Mode" : "Switch to Light Mode"}
|
||||
{isLightMode ? "Switch to Dark Mode" : "Switch to Light Mode"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Divider } from "@heroui/divider";
|
||||
import { Separator } from "@/components/shadcn";
|
||||
|
||||
export const AuthDivider = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<Divider className="flex-1" />
|
||||
<p className="text-tiny text-default-500 shrink-0">OR</p>
|
||||
<Divider className="flex-1" />
|
||||
<Separator className="flex-1" />
|
||||
<p className="text-text-neutral-tertiary shrink-0 text-xs">OR</p>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
|
||||
interface AuthFooterLinkProps {
|
||||
text: string;
|
||||
@@ -12,7 +12,7 @@ export const AuthFooterLink = ({
|
||||
href,
|
||||
}: AuthFooterLinkProps) => {
|
||||
return (
|
||||
<p className="text-small text-center">
|
||||
<p className="text-center text-sm">
|
||||
{text}
|
||||
<CustomLink size="md" href={href} target="_self">
|
||||
{linkText}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const AuthLayout = ({ title, children }: AuthLayoutProps) => {
|
||||
</div>
|
||||
|
||||
{/* Auth Form Container */}
|
||||
<div className="rounded-large border-divider shadow-small dark:bg-background/85 relative z-10 flex w-full max-w-sm flex-col gap-4 border bg-white/90 px-8 py-10 md:max-w-md">
|
||||
<div className="border-border-neutral-secondary dark:bg-bg-neutral-primary/85 relative z-10 flex w-full max-w-sm flex-col gap-4 rounded-[14px] border bg-white/90 px-8 py-10 shadow-sm md:max-w-md">
|
||||
{/* Header with Title and Theme Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="pb-2 text-xl font-medium">{title}</p>
|
||||
|
||||
@@ -13,10 +13,11 @@ import { AuthFooterLink } from "@/components/auth/oss/auth-footer-link";
|
||||
import { AuthLayout } from "@/components/auth/oss/auth-layout";
|
||||
import { SocialButtons } from "@/components/auth/oss/social-buttons";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { CustomInput } from "@/components/shadcn/custom";
|
||||
import { Form } from "@/components/shadcn/form";
|
||||
import { getSafeCallbackPath } from "@/lib/auth-callback-url";
|
||||
import { stripPasswordManagerHighlight } from "@/lib/password-manager";
|
||||
import { SignInFormData, signInSchema } from "@/types";
|
||||
|
||||
export const SignInForm = ({
|
||||
@@ -147,6 +148,7 @@ export const SignInForm = ({
|
||||
<AuthLayout title={title}>
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={stripPasswordManagerHighlight}
|
||||
noValidate
|
||||
method="post"
|
||||
className="flex flex-col gap-4"
|
||||
@@ -196,7 +198,7 @@ export const SignInForm = ({
|
||||
>
|
||||
{!isSamlMode && (
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
className="text-text-neutral-tertiary"
|
||||
icon="mdi:shield-key"
|
||||
width={24}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Checkbox } from "@heroui/checkbox";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
@@ -16,16 +15,17 @@ import { AuthFooterLink } from "@/components/auth/oss/auth-footer-link";
|
||||
import { AuthLayout } from "@/components/auth/oss/auth-layout";
|
||||
import { PasswordRequirementsMessage } from "@/components/auth/oss/password-validator";
|
||||
import { SocialButtons } from "@/components/auth/oss/social-buttons";
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { Button, Checkbox } from "@/components/shadcn";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { CustomInput } from "@/components/shadcn/custom";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
} from "@/components/shadcn/form";
|
||||
import { stripPasswordManagerHighlight } from "@/lib/password-manager";
|
||||
import { ApiError, SignUpFormData, signUpSchema } from "@/types";
|
||||
|
||||
const AUTH_ERROR_PATHS = {
|
||||
@@ -164,6 +164,7 @@ export const SignUpForm = ({
|
||||
<AuthLayout title="Sign up">
|
||||
<Form {...form}>
|
||||
<form
|
||||
ref={stripPasswordManagerHighlight}
|
||||
noValidate
|
||||
method="post"
|
||||
className="flex flex-col gap-4"
|
||||
@@ -217,14 +218,18 @@ export const SignUpForm = ({
|
||||
name="termsAndConditions"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
isRequired
|
||||
className="py-4"
|
||||
size="sm"
|
||||
checked={field.value}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
color="default"
|
||||
<div className="flex items-center gap-2 py-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
id="termsAndConditions"
|
||||
size="sm"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<label
|
||||
htmlFor="termsAndConditions"
|
||||
className="cursor-pointer text-sm"
|
||||
>
|
||||
I agree with the
|
||||
<CustomLink
|
||||
@@ -234,8 +239,9 @@ export const SignUpForm = ({
|
||||
Terms of Service
|
||||
</CustomLink>
|
||||
of Prowler
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
<span className="text-text-error-primary">*</span>
|
||||
</label>
|
||||
</div>
|
||||
<FormMessage className="text-text-error" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { CustomLink } from "@/components/shadcn/custom/custom-link";
|
||||
import { appendCallbackState } from "@/lib/auth-callback-url";
|
||||
|
||||
type SocialProvider = {
|
||||
@@ -73,7 +73,7 @@ const SocialButton = ({
|
||||
{provider.isOAuthEnabled ? (
|
||||
disabledTooltipContent
|
||||
) : (
|
||||
<div className="flex-inline text-small">
|
||||
<div className="flex-inline text-sm">
|
||||
{provider.disabledDocs.message}{" "}
|
||||
<CustomLink href={provider.disabledDocs.href}>
|
||||
Read the docs
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
SkeletonTableFindings,
|
||||
} from "@/components/findings/table";
|
||||
import { Alert, AlertDescription, Button } from "@/components/shadcn";
|
||||
import { Accordion } from "@/components/ui/accordion/Accordion";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { Accordion } from "@/components/shadcn/accordion/Accordion";
|
||||
import { DataTable } from "@/components/shadcn/table";
|
||||
import { FINDINGS_DEFAULT_SORT, MUTED_FILTER } from "@/lib";
|
||||
import { INVALID_CONFIG_NOTE } from "@/lib/compliance/commons";
|
||||
import { getComplianceMapper } from "@/lib/compliance/compliance-mapper";
|
||||
@@ -122,7 +122,7 @@ export const ClientAccordionContent = ({
|
||||
key: "checks",
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary">{checks.length}</span>
|
||||
<span className="text-button-primary">{checks.length}</span>
|
||||
{checks.length > 1 ? <span>Checks</span> : <span>Check</span>}
|
||||
</div>
|
||||
),
|
||||
@@ -197,7 +197,7 @@ export const ClientAccordionContent = ({
|
||||
items={accordionChecksItems}
|
||||
variant="light"
|
||||
defaultExpandedKeys={[""]}
|
||||
className="dark:bg-prowler-blue-400 rounded-lg bg-gray-50"
|
||||
className="dark:bg-bg-neutral-secondary rounded-lg bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn";
|
||||
import { Accordion, AccordionItemProps } from "@/components/ui";
|
||||
import { Accordion, AccordionItemProps } from "@/components/shadcn";
|
||||
|
||||
export const ClientAccordionWrapper = ({
|
||||
items,
|
||||
@@ -71,7 +71,7 @@ export const ClientAccordionWrapper = ({
|
||||
lastScrolledKeyRef.current = scrollToKey;
|
||||
// Two nested rAFs: the first lets the accordion children commit to
|
||||
// the DOM, the second lands after the browser has run a layout pass
|
||||
// so HeroUI's framer-motion expand has settled enough for
|
||||
// so the Collapsible CSS expand animation has settled enough for
|
||||
// scrollIntoView to read a stable offset.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { InfoTooltip } from "@/components/shadcn/info-field/info-field";
|
||||
import { FindingStatus, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { FindingStatus, StatusFindingBadge } from "@/components/shadcn/table";
|
||||
import { INVALID_CONFIG_NOTE } from "@/lib/compliance/commons";
|
||||
|
||||
interface ComplianceAccordionRequirementTitleProps {
|
||||
@@ -19,7 +19,7 @@ export const ComplianceAccordionRequirementTitle = ({
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-5/6 items-center gap-2">
|
||||
{type && (
|
||||
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
|
||||
<span className="bg-button-primary/10 text-button-primary rounded-md px-2 py-0.5 text-xs font-medium">
|
||||
{type}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip } from "@heroui/tooltip";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/shadcn";
|
||||
|
||||
interface ComplianceAccordionTitleProps {
|
||||
label: string;
|
||||
@@ -43,72 +43,63 @@ export const ComplianceAccordionTitle = ({
|
||||
{total > 0 ? (
|
||||
<div className="flex w-full">
|
||||
{pass > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="inline-block h-full bg-[#3CEC6D] transition-all duration-200 hover:brightness-110"
|
||||
style={{
|
||||
width: `${passPercentage}%`,
|
||||
marginRight: pass > 0 ? "2px" : "0",
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div className="px-1 py-0.5">
|
||||
<div className="text-xs font-medium">Pass</div>
|
||||
<div className="text-tiny text-default-400">
|
||||
<div className="text-text-neutral-tertiary text-xs">
|
||||
{pass} ({passPercentage.toFixed(1)}%)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
size="sm"
|
||||
placement="top"
|
||||
delay={0}
|
||||
closeDelay={0}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-full bg-[#3CEC6D] transition-all duration-200 hover:brightness-110"
|
||||
style={{
|
||||
width: `${passPercentage}%`,
|
||||
marginRight: pass > 0 ? "2px" : "0",
|
||||
}}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{fail > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="inline-block h-full bg-[#FB718F] transition-all duration-200 hover:brightness-110"
|
||||
style={{
|
||||
width: `${failPercentage}%`,
|
||||
marginRight: manual > 0 ? "2px" : "0",
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div className="px-1 py-0.5">
|
||||
<div className="text-xs font-medium">Fail</div>
|
||||
<div className="text-tiny text-default-400">
|
||||
<div className="text-text-neutral-tertiary text-xs">
|
||||
{fail} ({failPercentage.toFixed(1)}%)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
size="sm"
|
||||
placement="top"
|
||||
delay={0}
|
||||
closeDelay={0}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-full bg-[#FB718F] transition-all duration-200 hover:brightness-110"
|
||||
style={{
|
||||
width: `${failPercentage}%`,
|
||||
marginRight: manual > 0 ? "2px" : "0",
|
||||
}}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{manual > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className="inline-block h-full bg-[#868994] transition-all duration-200 hover:brightness-110"
|
||||
style={{ width: `${manualPercentage}%` }}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div className="px-1 py-0.5">
|
||||
<div className="text-xs font-medium">Manual</div>
|
||||
<div className="text-tiny text-default-400">
|
||||
<div className="text-text-neutral-tertiary text-xs">
|
||||
{manual} ({manualPercentage.toFixed(1)}%)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
size="sm"
|
||||
placement="top"
|
||||
delay={0}
|
||||
closeDelay={0}
|
||||
>
|
||||
<span
|
||||
className="inline-block h-full bg-[#868994] transition-all duration-200 hover:brightness-110"
|
||||
style={{ width: `${manualPercentage}%` }}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
@@ -117,19 +108,18 @@ export const ComplianceAccordionTitle = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
content={
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-text-neutral-secondary min-w-[32px] text-center text-xs font-medium">
|
||||
{total > 0 ? total : "—"}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<div className="px-1 py-0.5">
|
||||
<div className="text-xs font-medium">Total requirements</div>
|
||||
<div className="text-tiny text-default-400">{total}</div>
|
||||
<div className="text-text-neutral-tertiary text-xs">{total}</div>
|
||||
</div>
|
||||
}
|
||||
size="sm"
|
||||
placement="top"
|
||||
>
|
||||
<span className="text-default-600 min-w-[32px] text-center text-xs font-medium">
|
||||
{total > 0 ? total : "—"}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,11 +18,6 @@ describe("ComplianceCard", () => {
|
||||
expect(source).not.toContain("sm:flex-row");
|
||||
});
|
||||
|
||||
it("uses the shadcn progress component instead of Hero UI", () => {
|
||||
expect(source).toContain('from "@/components/shadcn/progress"');
|
||||
expect(source).not.toContain("@heroui/progress");
|
||||
});
|
||||
|
||||
it("places compact actions in the icon column on larger screens", () => {
|
||||
expect(source).toContain('orientation="column"');
|
||||
expect(source).toContain('buttonWidth="icon"');
|
||||
|
||||
@@ -131,7 +131,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<h4 className="text-small truncate leading-5 font-bold">
|
||||
<h4 className="truncate text-sm leading-5 font-bold">
|
||||
{formatTitle(title)}
|
||||
{version ? ` - ${version}` : ""}
|
||||
</h4>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@heroui/theme";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CategoryData } from "@/types/compliance";
|
||||
|
||||
interface HeatmapChartProps {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Progress } from "@heroui/progress";
|
||||
|
||||
import type { SectionScores } from "@/actions/overview/threat-score";
|
||||
import { RadialChart } from "@/components/graphs/radial-chart";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
|
||||
import { Progress } from "@/components/shadcn/progress";
|
||||
import {
|
||||
getScoreColor,
|
||||
getScoreIndicatorClass,
|
||||
getScoreLabel,
|
||||
getScoreLevel,
|
||||
getScoreTextClass,
|
||||
@@ -72,7 +72,7 @@ export function ThreatScoreBreakdownCard({
|
||||
{/* Pillar Breakdown */}
|
||||
<Card variant="inner" padding="sm" className="min-w-0 flex-1">
|
||||
<div className="mb-2">
|
||||
<span className="text-default-600 text-xs font-medium tracking-wide uppercase">
|
||||
<span className="text-text-neutral-secondary text-xs font-medium tracking-wide uppercase">
|
||||
Score by Pillar
|
||||
</span>
|
||||
</div>
|
||||
@@ -80,7 +80,9 @@ export function ThreatScoreBreakdownCard({
|
||||
{pillars.map(({ name, score, hasData }) => (
|
||||
<div key={name} className="space-y-0.5">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-default-700 truncate pr-2">{name}</span>
|
||||
<span className="text-text-neutral-secondary truncate pr-2">
|
||||
{name}
|
||||
</span>
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
hasData
|
||||
@@ -94,8 +96,9 @@ export function ThreatScoreBreakdownCard({
|
||||
<Progress
|
||||
aria-label={`${name} score`}
|
||||
value={hasData ? score : 0}
|
||||
color={getScoreColor(score)}
|
||||
size="md"
|
||||
indicatorClassName={getScoreIndicatorClass(
|
||||
getScoreColor(score),
|
||||
)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -113,22 +116,22 @@ export function ThreatScoreBreakdownCardSkeleton() {
|
||||
return (
|
||||
<Card variant="base" className="flex h-full w-full animate-pulse flex-col">
|
||||
<CardHeader>
|
||||
<div className="bg-default-200 h-5 w-40 rounded" />
|
||||
<div className="bg-border-neutral-tertiary h-5 w-40 rounded" />
|
||||
</CardHeader>
|
||||
{/* Mobile: vertical, Tablet: horizontal, Desktop: vertical */}
|
||||
<CardContent className="flex flex-1 flex-col gap-4 md:flex-row md:items-stretch lg:flex-col">
|
||||
<div className="flex w-full flex-col items-center justify-center md:w-[160px] md:flex-shrink-0 lg:w-full">
|
||||
<div className="bg-default-200 mx-auto h-[140px] w-[140px] rounded-full" />
|
||||
<div className="bg-border-neutral-tertiary mx-auto h-[140px] w-[140px] rounded-full" />
|
||||
</div>
|
||||
<Card variant="inner" padding="sm" className="min-w-0 flex-1">
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: SKELETON_PILLAR_COUNT }, (_, i) => (
|
||||
<div key={i} className="space-y-0.5">
|
||||
<div className="flex justify-between">
|
||||
<div className="bg-default-200 h-3 w-28 rounded" />
|
||||
<div className="bg-default-200 h-3 w-10 rounded" />
|
||||
<div className="bg-border-neutral-tertiary h-3 w-28 rounded" />
|
||||
<div className="bg-border-neutral-tertiary h-3 w-10 rounded" />
|
||||
</div>
|
||||
<div className="bg-default-200 h-2 w-full rounded" />
|
||||
<div className="bg-border-neutral-tertiary h-2 w-full rounded" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user