- {/* Prowler Logo */}
-
-
{/* Header with Title and Theme Toggle */}
{title}
diff --git a/ui/components/auth/oss/password-validator.tsx b/ui/components/auth/oss/password-validator.tsx
index 020a65bedc..e5703aee6a 100644
--- a/ui/components/auth/oss/password-validator.tsx
+++ b/ui/components/auth/oss/password-validator.tsx
@@ -58,21 +58,17 @@ export const PasswordRequirementsMessage = ({
return (
{allRequirementsMet ? (
-
+
Password meets all requirements
@@ -80,10 +76,10 @@ export const PasswordRequirementsMessage = ({
-
+
Password must include:
@@ -99,12 +95,12 @@ export const PasswordRequirementsMessage = ({
{req.label}
diff --git a/ui/components/auth/oss/sign-up-form.tsx b/ui/components/auth/oss/sign-up-form.tsx
index 723b283196..2cca1e9a15 100644
--- a/ui/components/auth/oss/sign-up-form.tsx
+++ b/ui/components/auth/oss/sign-up-form.tsx
@@ -3,7 +3,7 @@
import { Checkbox } from "@heroui/checkbox";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
-import { useForm } from "react-hook-form";
+import { useForm, useWatch } from "react-hook-form";
import { createNewUser } from "@/actions/auth";
import { AuthDivider } from "@/components/auth/oss/auth-divider";
@@ -62,6 +62,12 @@ export const SignUpForm = ({
},
});
+ const passwordValue = useWatch({
+ control: form.control,
+ name: "password",
+ defaultValue: "",
+ });
+
const isLoading = form.formState.isSubmitting;
const onSubmit = async (data: SignUpFormData) => {
@@ -152,9 +158,7 @@ export const SignUpForm = ({
showFormMessage
/>
-
+
{!hideExpandButton && (
-
+
)}
{
return (
-
-
-
+
+
+
+
+
- {score.toFixed(1)}%
+ {score}%
);
},
+ enableSorting: false,
},
{
accessorKey: "region",
- header: "Region",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const region = getResourceData(row, "region");
@@ -165,18 +189,24 @@ export const ColumnNewFindingsToDate: ColumnDef
[] = [
);
},
+ enableSorting: false,
},
{
accessorKey: "service",
- header: "Service",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const { servicename } = getFindingsMetadata(row);
return
{servicename}
;
},
+ enableSorting: false,
},
{
accessorKey: "cloudProvider",
- header: "Cloud Provider",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const provider = getProviderData(row, "provider");
const alias = getProviderData(row, "alias");
@@ -184,7 +214,7 @@ export const ColumnNewFindingsToDate: ColumnDef
[] = [
return (
<>
- [] = [
>
);
},
+ enableSorting: false,
},
];
diff --git a/ui/components/providers/table/column-providers.tsx b/ui/components/providers/table/column-providers.tsx
index 0162a5a09b..47748f22ac 100644
--- a/ui/components/providers/table/column-providers.tsx
+++ b/ui/components/providers/table/column-providers.tsx
@@ -45,21 +45,27 @@ export const ColumnProviders: ColumnDef[] = [
},
{
accessorKey: "scanJobs",
- header: "Scan Jobs",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const {
attributes: { uid },
} = getProviderData(row);
return ;
},
+ enableSorting: false,
},
{
accessorKey: "groupNames",
- header: "Groups",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const { groupNames } = getProviderData(row);
return ;
},
+ enableSorting: false,
},
{
accessorKey: "uid",
@@ -95,9 +101,11 @@ export const ColumnProviders: ColumnDef[] = [
},
{
id: "actions",
+ header: ({ column }) => ,
cell: ({ row }) => {
return ;
},
+ enableSorting: false,
},
];
diff --git a/ui/components/resources/table/column-resources.tsx b/ui/components/resources/table/column-resources.tsx
index 6739f17103..d26fe4f5e5 100644
--- a/ui/components/resources/table/column-resources.tsx
+++ b/ui/components/resources/table/column-resources.tsx
@@ -5,7 +5,7 @@ import { Database } from "lucide-react";
import { useSearchParams } from "next/navigation";
import { InfoIcon } from "@/components/icons";
-import { EntityInfoShort, SnippetChip } from "@/components/ui/entities";
+import { EntityInfo, SnippetChip } from "@/components/ui/entities";
import { TriggerSheet } from "@/components/ui/sheet";
import { DataTableColumnHeader } from "@/components/ui/table";
import { ProviderType, ResourceProps } from "@/types";
@@ -62,12 +62,17 @@ const ResourceDetailsCell = ({ row }: { row: any }) => {
export const ColumnResources: ColumnDef[] = [
{
id: "moreInfo",
- header: "Details",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => ,
+ enableSorting: false,
},
{
accessorKey: "resourceName",
- header: "Resource name",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const resourceName = getResourceData(row, "name");
const displayName =
@@ -83,10 +88,13 @@ export const ColumnResources: ColumnDef[] = [
/>
);
},
+ enableSorting: false,
},
{
accessorKey: "failedFindings",
- header: () => Failed Findings
,
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const failedFindingsCount = getResourceData(
row,
@@ -94,17 +102,14 @@ export const ColumnResources: ColumnDef[] = [
) as number;
return (
- <>
-
-
- {failedFindingsCount}
-
-
- >
+
+ {failedFindingsCount}
+
);
},
+ enableSorting: false,
},
{
accessorKey: "region",
@@ -157,14 +162,16 @@ export const ColumnResources: ColumnDef[] = [
},
{
accessorKey: "provider",
- header: "Cloud Provider",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const provider = getProviderData(row, "provider");
const alias = getProviderData(row, "alias");
const uid = getProviderData(row, "uid");
return (
<>
- [] = [
>
);
},
+ enableSorting: false,
},
];
diff --git a/ui/components/roles/table/column-roles.tsx b/ui/components/roles/table/column-roles.tsx
index 859e02af48..7faf340aa9 100644
--- a/ui/components/roles/table/column-roles.tsx
+++ b/ui/components/roles/table/column-roles.tsx
@@ -105,10 +105,13 @@ export const ColumnsRoles: ColumnDef[] = [
},
{
accessorKey: "actions",
- header: () => Actions
,
+ header: ({ column }) => (
+
+ ),
id: "actions",
cell: ({ row }) => {
return ;
},
+ enableSorting: false,
},
];
diff --git a/ui/components/scans/launch-workflow/select-scan-provider.tsx b/ui/components/scans/launch-workflow/select-scan-provider.tsx
index 3fc7119edf..a817565a50 100644
--- a/ui/components/scans/launch-workflow/select-scan-provider.tsx
+++ b/ui/components/scans/launch-workflow/select-scan-provider.tsx
@@ -9,7 +9,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/shadcn";
-import { EntityInfoShort } from "@/components/ui/entities";
+import { EntityInfo } from "@/components/ui/entities";
import { FormControl, FormField, FormMessage } from "@/components/ui/form";
interface SelectScanProviderProps<
@@ -54,7 +54,7 @@ export const SelectScanProvider = <
{selectedItem ? (
-
) : (
"Choose a cloud provider"
@@ -74,7 +74,7 @@ export const SelectScanProvider = <
{providers.map((item) => (
-
))}
diff --git a/ui/components/scans/table/scan-detail.tsx b/ui/components/scans/table/scan-detail.tsx
index cc8589e462..0cedd7e9c0 100644
--- a/ui/components/scans/table/scan-detail.tsx
+++ b/ui/components/scans/table/scan-detail.tsx
@@ -2,11 +2,8 @@
import { Snippet } from "@heroui/snippet";
-import {
- DateWithTime,
- EntityInfoShort,
- InfoField,
-} from "@/components/ui/entities";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
+import { DateWithTime, EntityInfo, InfoField } from "@/components/ui/entities";
import { StatusBadge } from "@/components/ui/table/status-badge";
import { ProviderProps, ProviderType, ScanProps, TaskDetails } from "@/types";
@@ -28,21 +25,6 @@ const formatDuration = (seconds: number) => {
return parts.join(" ");
};
-const Section = ({
- title,
- children,
-}: {
- title: string;
- children: React.ReactNode;
-}) => (
-
-
- {title}
-
- {children}
-
-);
-
export const ScanDetail = ({
scanDetails,
}: {
@@ -68,7 +50,7 @@ export const ScanDetail = ({
loadingProgress={scan.progress}
/>
-
{/* Scan Details */}
-
-
- {renderValue(scan.name)}
-
- {scan.unique_resource_count}
-
- {scan.progress}%
-
+
+
+ Scan Details
+
+
+
+ {renderValue(scan.name)}
+
+ {scan.unique_resource_count}
+
+ {scan.progress}%
+
-
- {renderValue(scan.trigger)}
- {renderValue(scan.state)}
-
- {formatDuration(scan.duration)}
-
-
+
+ {renderValue(scan.trigger)}
+ {renderValue(scan.state)}
+
+ {formatDuration(scan.duration)}
+
+
-
-
- {scanDetails.id}
-
-
+
+ {scanDetails.id}
+
- {scan.state === "failed" && taskDetails?.attributes.result && (
- <>
- {taskDetails.attributes.result.exc_message && (
-
-
-
- {taskDetails.attributes.result.exc_message.join("\n")}
-
-
-
- )}
-
-
- {renderValue(taskDetails.attributes.result.exc_type)}
-
-
- >
- )}
+ {scan.state === "failed" && taskDetails?.attributes.result && (
+ <>
+ {taskDetails.attributes.result.exc_message && (
+
+
+
+ {taskDetails.attributes.result.exc_message.join("\n")}
+
+
+
+ )}
+
+
+ {renderValue(taskDetails.attributes.result.exc_type)}
+
+
+ >
+ )}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx
index f3f2e3ac62..5f55628b29 100644
--- a/ui/components/scans/table/scans/column-get-scans.tsx
+++ b/ui/components/scans/table/scans/column-get-scans.tsx
@@ -1,12 +1,11 @@
"use client";
-import { Tooltip } from "@heroui/tooltip";
import { ColumnDef } from "@tanstack/react-table";
import { useRouter, useSearchParams } from "next/navigation";
import { InfoIcon } from "@/components/icons";
import { TableLink } from "@/components/ui/custom";
-import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
+import { DateWithTime, EntityInfo } from "@/components/ui/entities";
import { TriggerSheet } from "@/components/ui/sheet";
import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
import { ProviderType, ScanProps } from "@/types";
@@ -67,12 +66,17 @@ const ScanDetailsCell = ({ row }: { row: any }) => {
export const ColumnGetScans: ColumnDef
[] = [
{
id: "moreInfo",
- header: "Details",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => ,
+ enableSorting: false,
},
{
accessorKey: "cloudProvider",
- header: () => Cloud Provider
,
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const providerInfo = row.original.providerInfo;
@@ -83,18 +87,21 @@ export const ColumnGetScans: ColumnDef[] = [
const { provider, uid, alias } = providerInfo;
return (
-
);
},
+ enableSorting: false,
},
{
accessorKey: "started_at",
- header: () => Started at
,
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const {
attributes: { started_at },
@@ -106,10 +113,13 @@ export const ColumnGetScans: ColumnDef[] = [
);
},
+ enableSorting: false,
},
{
accessorKey: "status",
- header: "Status",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const {
attributes: { state },
@@ -123,10 +133,13 @@ export const ColumnGetScans: ColumnDef
[] = [
);
},
+ enableSorting: false,
},
{
accessorKey: "findings",
- header: "Findings",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const { id } = getScanData(row);
const scanState = row.original.attributes?.state;
@@ -138,10 +151,13 @@ export const ColumnGetScans: ColumnDef
[] = [
/>
);
},
+ enableSorting: false,
},
{
accessorKey: "compliance",
- header: "Compliance",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const { id } = getScanData(row);
const scanState = row.original.attributes?.state;
@@ -153,21 +169,12 @@ export const ColumnGetScans: ColumnDef[] = [
/>
);
},
+ enableSorting: false,
},
{
id: "download",
- header: () => (
-
+ header: ({ column }) => (
+
),
cell: ({ row }) => {
return (
@@ -176,21 +183,13 @@ export const ColumnGetScans: ColumnDef[] = [
);
},
+ enableSorting: false,
},
-
- // {
- // accessorKey: "scanner_args",
- // header: "Scanner Args",
- // cell: ({ row }) => {
- // const {
- // attributes: { scanner_args },
- // } = getScanData(row);
- // return
{scanner_args?.only_logs}
;
- // },
- // },
{
accessorKey: "resources",
- header: "Resources",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const {
attributes: { unique_resource_count },
@@ -201,16 +200,20 @@ export const ColumnGetScans: ColumnDef
[] = [
);
},
+ enableSorting: false,
},
{
accessorKey: "scheduled_at",
- header: "Scheduled at",
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const {
attributes: { scheduled_at },
} = getScanData(row);
return
;
},
+ enableSorting: false,
},
{
accessorKey: "completed_at",
@@ -272,8 +275,10 @@ export const ColumnGetScans: ColumnDef
[] = [
},
{
id: "actions",
+ header: ({ column }) => ,
cell: ({ row }) => {
return ;
},
+ enableSorting: false,
},
];
diff --git a/ui/components/scans/table/scans/skeleton-scan-detail.tsx b/ui/components/scans/table/scans/skeleton-scan-detail.tsx
index 5e69becb48..82645b8204 100644
--- a/ui/components/scans/table/scans/skeleton-scan-detail.tsx
+++ b/ui/components/scans/table/scans/skeleton-scan-detail.tsx
@@ -1,58 +1,64 @@
+import { Card, CardContent, CardHeader } from "@/components/shadcn";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
+
export const SkeletonScanDetail = () => {
return (
{/* Header Skeleton */}
{/* Scan Details Section Skeleton */}
-
-
+
+
+
+
+
+ {/* First grid row */}
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+
+
+
+ ))}
+
- {/* First grid row */}
-
- {Array.from({ length: 3 }).map((_, index) => (
-
- ))}
-
+ {/* Second grid row */}
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+
+
+
+ ))}
+
- {/* Second grid row */}
-
- {Array.from({ length: 3 }).map((_, index) => (
-
- ))}
-
+ {/* Scan ID field */}
+
+
+
+
- {/* Scan ID field */}
-
-
- {/* Third grid row */}
-
- {Array.from({ length: 3 }).map((_, index) => (
-
- ))}
-
-
+ {/* Third grid row */}
+
+ {Array.from({ length: 3 }).map((_, index) => (
+
+
+
+
+ ))}
+
+
+
);
};
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx
index bb42f4bfff..769c412b8e 100644
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx
+++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx
@@ -103,7 +103,7 @@ export const ResourceStatsCard = ({
>
{header && }
{emptyState ? (
-
+
{emptyState.message}
diff --git a/ui/components/shadcn/combobox/combobox.tsx b/ui/components/shadcn/combobox/combobox.tsx
index 115c830d31..b10bcf8973 100644
--- a/ui/components/shadcn/combobox/combobox.tsx
+++ b/ui/components/shadcn/combobox/combobox.tsx
@@ -1,7 +1,7 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
-import { Check, ChevronsUpDown } from "lucide-react";
+import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/shadcn/button/button";
@@ -72,6 +72,8 @@ export interface ComboboxProps
contentClassName?: string;
disabled?: boolean;
showSelectedFirst?: boolean;
+ loading?: boolean;
+ loadingMessage?: string;
}
export function Combobox({
@@ -88,6 +90,8 @@ export function Combobox({
variant = "default",
disabled = false,
showSelectedFirst = true,
+ loading = false,
+ loadingMessage = "Loading...",
}: ComboboxProps) {
const [open, setOpen] = useState(false);
@@ -127,8 +131,16 @@ export function Combobox({
align="start"
>
-
+ {!loading && (
+
+ )}
+ {loading && (
+
+
+ {loadingMessage}
+
+ )}
{emptyMessage}
{/* Show selected option first if enabled */}
diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts
index b47617530b..2186db85c2 100644
--- a/ui/components/shadcn/index.ts
+++ b/ui/components/shadcn/index.ts
@@ -12,3 +12,4 @@ export * from "./separator/separator";
export * from "./skeleton/skeleton";
export * from "./tabs/generic-tabs";
export * from "./tabs/tabs";
+export * from "./tooltip";
diff --git a/ui/components/shadcn/select/select.tsx b/ui/components/shadcn/select/select.tsx
index b56e3f186c..8c2b12d283 100644
--- a/ui/components/shadcn/select/select.tsx
+++ b/ui/components/shadcn/select/select.tsx
@@ -131,13 +131,15 @@ function SelectItem({
- {children}
+
+ {children}
+
diff --git a/ui/components/ui/accordion/Accordion.tsx b/ui/components/ui/accordion/Accordion.tsx
index efb390bda7..1907c940c4 100644
--- a/ui/components/ui/accordion/Accordion.tsx
+++ b/ui/components/ui/accordion/Accordion.tsx
@@ -120,7 +120,10 @@ export const Accordion = ({
return (
}
classNames={{
- base: index === 0 || index === 1 ? "my-1" : "my-1",
+ base: index === 0 || index === 1 ? "my-2" : "my-2",
title: "text-sm",
subtitle: "text-xs text-gray-500",
trigger:
diff --git a/ui/components/ui/entities/entity-info-short.tsx b/ui/components/ui/entities/entity-info-short.tsx
deleted file mode 100644
index e5e0621878..0000000000
--- a/ui/components/ui/entities/entity-info-short.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Tooltip } from "@heroui/tooltip";
-import React from "react";
-
-import { IdIcon } from "@/components/icons";
-import { ProviderType } from "@/types";
-
-import { getProviderLogo } from "./get-provider-logo";
-import { SnippetChip } from "./snippet-chip";
-
-interface EntityInfoProps {
- cloudProvider: ProviderType;
- entityAlias?: string;
- entityId?: string;
- hideCopyButton?: boolean;
- snippetWidth?: string;
- showConnectionStatus?: boolean;
- maxWidth?: string;
-}
-
-export const EntityInfoShort: React.FC = ({
- cloudProvider,
- entityAlias,
- entityId,
- hideCopyButton = false,
- showConnectionStatus = false,
- maxWidth = "max-w-[120px]",
-}) => {
- return (
-
-
-
- {getProviderLogo(cloudProvider)}
- {showConnectionStatus && (
-
-
-
- )}
-
-
- {entityAlias && (
-
-
- {entityAlias}
-
-
- )}
- }
- />
-
-
-
- );
-};
diff --git a/ui/components/ui/entities/entity-info.tsx b/ui/components/ui/entities/entity-info.tsx
new file mode 100644
index 0000000000..6d36eed011
--- /dev/null
+++ b/ui/components/ui/entities/entity-info.tsx
@@ -0,0 +1,110 @@
+"use client";
+
+import { Tooltip } from "@heroui/tooltip";
+import { useEffect, useState } from "react";
+
+import { CopyIcon, DoneIcon } from "@/components/icons";
+import type { ProviderType } from "@/types";
+
+import { getProviderLogo } from "./get-provider-logo";
+
+interface EntityInfoProps {
+ cloudProvider: ProviderType;
+ entityAlias?: string;
+ entityId?: string;
+ snippetWidth?: string;
+ showConnectionStatus?: boolean;
+ maxWidth?: string;
+ showCopyAction?: boolean;
+}
+
+export const EntityInfo = ({
+ cloudProvider,
+ entityAlias,
+ entityId,
+ showConnectionStatus = false,
+ maxWidth = "w-[120px]",
+ showCopyAction = true,
+}: EntityInfoProps) => {
+ const [copied, setCopied] = useState(false);
+
+ useEffect(() => {
+ if (!copied) return undefined;
+
+ const timer = setTimeout(() => setCopied(false), 1400);
+ return () => clearTimeout(timer);
+ }, [copied]);
+
+ const handleCopyEntityId = async () => {
+ if (!entityId) return;
+
+ try {
+ await navigator.clipboard.writeText(entityId);
+ setCopied(true);
+ } catch (_error) {
+ setCopied(false);
+ }
+ };
+
+ const canCopy = Boolean(entityId && showCopyAction);
+
+ return (
+
+
+ {getProviderLogo(cloudProvider)}
+ {showConnectionStatus && (
+
+
+
+ )}
+
+
+ {entityAlias ? (
+
+
+ {entityAlias}
+
+
+ ) : (
+
+
+ -
+
+
+ )}
+ {entityId && (
+
+
+
+ {entityId}
+
+
+ {canCopy && (
+
+
+
+ )}
+
+ )}
+
+
+ );
+};
diff --git a/ui/components/ui/entities/index.ts b/ui/components/ui/entities/index.ts
index fb13c99282..88cf9504f3 100644
--- a/ui/components/ui/entities/index.ts
+++ b/ui/components/ui/entities/index.ts
@@ -1,5 +1,5 @@
export * from "./date-with-time";
-export * from "./entity-info-short";
+export * from "./entity-info";
export * from "./get-provider-logo";
export * from "./info-field";
export * from "./scan-status";
diff --git a/ui/components/ui/table/data-table-column-header.tsx b/ui/components/ui/table/data-table-column-header.tsx
index 73947b5cbf..b5e6e71e93 100644
--- a/ui/components/ui/table/data-table-column-header.tsx
+++ b/ui/components/ui/table/data-table-column-header.tsx
@@ -75,14 +75,18 @@ export const DataTableColumnHeader = ({
};
if (!column.getCanSort()) {
- return {title}
;
+ return (
+
+ {title}
+
+ );
}
return (