@@ -145,90 +147,99 @@ export const LLMProvidersTable = () => {
const showConfigure = provider.isConnected;
return (
-
{/* Header */}
-
-
-
-
-
- {provider.provider}
-
- {provider.isDefaultProvider && (
-
- Default
-
- )}
+
+
+
+
+
+
+ {provider.provider}
+
+ {provider.isDefaultProvider && (
+
+ Default
+
+ )}
+
+
+ {provider.description}
+
-
- {provider.description}
-
-
+
- {/* Status and Model Info */}
-
-
-
- Status
-
-
- {provider.isConnected
- ? provider.isActive
- ? "Connected"
- : "Connection Failed"
- : "Not configured"}
-
-
-
- {provider.defaultModel && (
+
+ {/* Status and Model Info */}
+
-
- Default Model
+
+ Status
-
- {provider.defaultModel}
+
+ {provider.isConnected
+ ? provider.isActive
+ ? "Connected"
+ : "Connection Failed"
+ : "Not configured"}
+
+ {provider.defaultModel && (
+
+
+ Default Model
+
+
+ {provider.defaultModel}
+
+
+ )}
+
+
+ {/* Action Button */}
+ {showConnect && (
+
+
+ Connect
+
+
)}
-
- {/* Action Button */}
- {showConnect && (
-
- Connect
-
- )}
-
- {showConfigure && (
-
- Configure
-
- )}
-
+ {showConfigure && (
+
+
+ Configure
+
+
+ )}
+
+
);
})}
diff --git a/ui/components/lighthouse/loader.tsx b/ui/components/lighthouse/loader.tsx
index 624405797e..c0b3cd60f8 100644
--- a/ui/components/lighthouse/loader.tsx
+++ b/ui/components/lighthouse/loader.tsx
@@ -41,7 +41,7 @@ const Loader = ({
>
{text &&
{text} }
{text || "Loading..."}
diff --git a/ui/components/lighthouse/select-model.tsx b/ui/components/lighthouse/select-model.tsx
index d260ee5f59..95e8718b62 100644
--- a/ui/components/lighthouse/select-model.tsx
+++ b/ui/components/lighthouse/select-model.tsx
@@ -8,7 +8,7 @@ import {
getTenantConfig,
updateTenantConfig,
} from "@/actions/lighthouse/lighthouse";
-import { CustomButton } from "@/components/ui/custom";
+import { Button } from "@/components/shadcn";
import type { LighthouseProvider } from "@/types/lighthouse";
import {
@@ -155,7 +155,7 @@ export const SelectModel = ({
{isEditMode ? "Update Default Model" : "Select Default Model"}
-
+
{isEditMode
? "Update the default model to use with this provider."
: "Choose the default model to use with this provider."}
@@ -164,7 +164,7 @@ export const SelectModel = ({
fetchModels(true)}
disabled={isLoading}
- className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-50 dark:text-gray-300 dark:hover:bg-gray-800"
+ className="text-text-neutral-secondary hover:bg-bg-neutral-tertiary flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-50"
aria-label="Refresh models"
>
{error && (
-
-
{error}
+
)}
@@ -185,14 +185,14 @@ export const SelectModel = ({
setSearchQuery(e.target.value)}
- className="w-full rounded-lg border border-gray-300 py-2.5 pr-4 pl-11 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-white"
+ className="border-border-neutral-primary bg-bg-neutral-primary focus:border-button-primary focus:ring-button-primary w-full rounded-lg border py-2.5 pr-4 pl-11 text-sm focus:ring-1 focus:outline-none"
/>
)}
@@ -201,32 +201,32 @@ export const SelectModel = ({
) : models.length === 0 ? (
-
-
+
+
No models available. Click refresh to fetch models.
) : filteredModels.length === 0 ? (
-
-
+
+
No models found matching "{searchQuery}"
) : (
-
+
{filteredModels.map((model) => (
@@ -241,7 +241,7 @@ export const SelectModel = ({
{model.name}
{isRecommended(model.id) && (
-
+
Recommended
@@ -255,17 +255,13 @@ export const SelectModel = ({
-
{isSaving ? "Saving..." : "Select"}
-
+
diff --git a/ui/components/lighthouse/workflow/workflow-connect-llm.tsx b/ui/components/lighthouse/workflow/workflow-connect-llm.tsx
index 3a4f8f02b7..d99ebd4811 100644
--- a/ui/components/lighthouse/workflow/workflow-connect-llm.tsx
+++ b/ui/components/lighthouse/workflow/workflow-connect-llm.tsx
@@ -64,20 +64,21 @@ export const WorkflowConnectLLM = () => {
classNames={{
base: "px-0.5 mb-5",
label: "text-small",
- value: "text-small text-default-400",
+ value: "text-small text-button-primary",
+ indicator: "bg-button-primary",
}}
label="Steps"
- maxValue={steps.length - 1}
+ maxValue={steps.length}
minValue={0}
showValueLabel={true}
size="md"
- value={currentStep}
+ value={currentStep + 1}
valueLabel={`${currentStep + 1} of ${steps.length}`}
/>
diff --git a/ui/components/manage-groups/forms/add-group-form.tsx b/ui/components/manage-groups/forms/add-group-form.tsx
index 6424ac8235..676bd85fca 100644
--- a/ui/components/manage-groups/forms/add-group-form.tsx
+++ b/ui/components/manage-groups/forms/add-group-form.tsx
@@ -6,12 +6,9 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { createProviderGroup } from "@/actions/manage-groups";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import {
- CustomButton,
- CustomDropdownSelection,
- CustomInput,
-} from "@/components/ui/custom";
+import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { ApiError } from "@/types";
@@ -123,7 +120,6 @@ export const AddGroupForm = ({
placeholder="Enter the provider group name"
variant="flat"
isRequired
- isInvalid={!!form.formState.errors.name}
/>
@@ -178,18 +174,10 @@ export const AddGroupForm = ({
{/* Submit Button */}
- }
- >
- {isLoading ? <>Loading> : Create Group }
-
+
+ {!isLoading && }
+ {isLoading ? "Loading" : "Create Group"}
+
diff --git a/ui/components/manage-groups/forms/delete-group-form.tsx b/ui/components/manage-groups/forms/delete-group-form.tsx
index e330cdfc6c..96911dbdda 100644
--- a/ui/components/manage-groups/forms/delete-group-form.tsx
+++ b/ui/components/manage-groups/forms/delete-group-form.tsx
@@ -8,8 +8,8 @@ import * as z from "zod";
import { deleteProviderGroup } from "@/actions/manage-groups/manage-groups";
import { DeleteIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import { CustomButton } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
const formSchema = z.object({
@@ -57,33 +57,26 @@ export const DeleteGroupForm = ({
diff --git a/ui/components/manage-groups/forms/edit-group-form.tsx b/ui/components/manage-groups/forms/edit-group-form.tsx
index 63a1dd7a00..2adca0be12 100644
--- a/ui/components/manage-groups/forms/edit-group-form.tsx
+++ b/ui/components/manage-groups/forms/edit-group-form.tsx
@@ -8,12 +8,9 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { updateProviderGroup } from "@/actions/manage-groups/manage-groups";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import {
- CustomButton,
- CustomDropdownSelection,
- CustomInput,
-} from "@/components/ui/custom";
+import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { ApiError } from "@/types";
@@ -160,7 +157,6 @@ export const EditGroupForm = ({
placeholder="Enter the provider group name"
variant="flat"
isRequired
- isInvalid={!!form.formState.errors.name}
/>
@@ -241,32 +237,21 @@ export const EditGroupForm = ({
)}
-
-
+ {
+ variant="ghost"
+ onClick={() => {
router.push("/manage-groups");
}}
- isDisabled={isLoading}
+ disabled={isLoading}
>
- Cancel
-
- }
- >
- {isLoading ? <>Loading> : Update Group }
-
+ Cancel
+
+
+ {!isLoading && }
+ {isLoading ? "Loading" : "Update Group"}
+
diff --git a/ui/components/manage-groups/manage-groups-button.tsx b/ui/components/manage-groups/manage-groups-button.tsx
index 7bfc0cdc0d..48df5ee607 100644
--- a/ui/components/manage-groups/manage-groups-button.tsx
+++ b/ui/components/manage-groups/manage-groups-button.tsx
@@ -1,20 +1,17 @@
"use client";
import { SettingsIcon } from "lucide-react";
+import Link from "next/link";
-import { CustomButton } from "../ui/custom";
+import { Button } from "@/components/shadcn";
export const ManageGroupsButton = () => {
return (
-
}
- >
- Manage Groups
-
+
+
+
+ Manage Groups
+
+
);
};
diff --git a/ui/components/manage-groups/table/data-table-row-actions.tsx b/ui/components/manage-groups/table/data-table-row-actions.tsx
index c5bd4f7ac0..af9d2e0ade 100644
--- a/ui/components/manage-groups/table/data-table-row-actions.tsx
+++ b/ui/components/manage-groups/table/data-table-row-actions.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Button } from "@heroui/button";
import {
Dropdown,
DropdownItem,
@@ -18,6 +17,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { VerticalDotsIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { CustomAlertModal } from "@/components/ui/custom";
import { DeleteGroupForm } from "../forms";
@@ -48,12 +48,12 @@ export function DataTableRowActions
({
-
-
+
+
({
}
onPress={() => setIsDeleteOpen(true)}
diff --git a/ui/components/manage-groups/table/skeleton-table-groups.tsx b/ui/components/manage-groups/table/skeleton-table-groups.tsx
index a9bdd1b5b6..03c817b9d1 100644
--- a/ui/components/manage-groups/table/skeleton-table-groups.tsx
+++ b/ui/components/manage-groups/table/skeleton-table-groups.tsx
@@ -1,33 +1,20 @@
-import { Card } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
import React from "react";
+import { Card } from "@/components/shadcn/card/card";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
+
export const SkeletonTableGroups = () => {
return (
-
+
{/* Table headers */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{/* Table body */}
@@ -35,29 +22,15 @@ export const SkeletonTableGroups = () => {
{[...Array(3)].map((_, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
))}
diff --git a/ui/components/overview/AttackSurface.tsx b/ui/components/overview/AttackSurface.tsx
deleted file mode 100644
index 495ceb3d1c..0000000000
--- a/ui/components/overview/AttackSurface.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from "react";
-
-import { ActionCard } from "@/components/ui";
-
-const cardData = [
- {
- findings: 3,
- title: "Internet Exposed Resources",
- },
- {
- findings: 15,
- title: "Exposed Secrets",
- },
- {
- findings: 0,
- title: "IAM Policies Leading to Privilege Escalation",
- },
- {
- findings: 0,
- title: "EC2 with Metadata Service V1 (IMDSv1)",
- },
-];
-
-export const AttackSurface = () => {
- return (
-
- {cardData.map((card, index) => (
-
0 ? "fail" : "success"}
- icon={
- card.findings > 0
- ? "solar:danger-triangle-bold"
- : "heroicons:shield-check-solid"
- }
- title={card.title}
- description={
- card.findings > 0 ? "Review Required" : "No Issues Found"
- }
- />
- ))}
-
- );
-};
diff --git a/ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx b/ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx
deleted file mode 100644
index 5566f234ed..0000000000
--- a/ui/components/overview/findings-by-severity-chart/findings-by-severity-chart.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-"use client";
-
-import { Card, CardBody } from "@heroui/card";
-import { Bar, BarChart, LabelList, XAxis, YAxis } from "recharts";
-
-import {
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "@/components/ui/chart/Chart";
-import { FindingsSeverityOverview } from "@/types/components";
-
-export interface ChartConfig {
- [key: string]: {
- label?: React.ReactNode;
- icon?: React.ComponentType;
- color?: string;
- theme?: string;
- link?: string;
- };
-}
-
-const chartConfig = {
- critical: {
- label: "Critical",
- color: "var(--color-bg-data-critical)",
- link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=critical",
- },
- high: {
- label: "High",
- color: "var(--color-bg-data-high)",
- link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=high",
- },
- medium: {
- label: "Medium",
- color: "var(--color-bg-data-medium)",
- link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=medium",
- },
- low: {
- label: "Low",
- color: "var(--color-bg-data-low)",
- link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=low",
- },
- informational: {
- label: "Informational",
- color: "var(--color-bg-data-info)",
- link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=informational",
- },
-} satisfies ChartConfig;
-
-export const FindingsBySeverityChart = ({
- findingsBySeverity,
-}: {
- findingsBySeverity: FindingsSeverityOverview;
-}) => {
- const defaultAttributes = {
- critical: 0,
- high: 0,
- medium: 0,
- low: 0,
- informational: 0,
- };
-
- const attributes = findingsBySeverity?.data?.attributes || defaultAttributes;
-
- const chartData = Object.entries(attributes).map(([severity, findings]) => ({
- severity,
- findings,
- fill: chartConfig[severity as keyof typeof chartConfig]?.color,
- }));
-
- return (
-
-
-
-
-
-
- chartConfig[value as keyof typeof chartConfig]?.label
- }
- />
-
-
-
- }
- />
- {
- const severity = data.severity as keyof typeof chartConfig;
- const link = chartConfig[severity]?.link;
- if (link) {
- window.location.href = link;
- }
- }}
- style={{ cursor: "pointer" }}
- >
- (value === 0 ? "" : value)}
- />
- (value === 0 ? "0" : "")}
- />
-
-
-
-
-
-
- );
-};
diff --git a/ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx b/ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx
deleted file mode 100644
index e5a328c1b8..0000000000
--- a/ui/components/overview/findings-by-severity-chart/skeleton-findings-severity-chart.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Card, CardBody, CardHeader } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
-
-export const SkeletonFindingsBySeverityChart = () => {
- return (
-
-
-
-
-
-
-
-
- {/* Critical */}
-
- {/* High */}
-
- {/* Medium */}
-
- {/* Low */}
-
- {/* Informational */}
-
-
-
-
- );
-};
diff --git a/ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx b/ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx
deleted file mode 100644
index 8c08d17144..0000000000
--- a/ui/components/overview/findings-by-status-chart/findings-by-status-chart.tsx
+++ /dev/null
@@ -1,278 +0,0 @@
-"use client";
-
-import { Card, CardBody } from "@heroui/card";
-import { Chip } from "@heroui/chip";
-import { TrendingUp } from "lucide-react";
-import Link from "next/link";
-import { useSearchParams } from "next/navigation";
-import React, { useMemo } from "react";
-import { Label, Pie, PieChart } from "recharts";
-
-import { MutedIcon, NotificationIcon, SuccessIcon } from "@/components/icons";
-import {
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "@/components/ui/chart/Chart";
-
-const calculatePercent = (
- chartData: { findings: string; number: number; fill: string }[],
-) => {
- const total = chartData.reduce((sum, item) => sum + item.number, 0);
-
- return chartData.map((item) => ({
- ...item,
- percent: total > 0 ? Math.round((item.number / total) * 100) + "%" : "0%",
- }));
-};
-
-interface FindingsByStatusChartProps {
- findingsByStatus: {
- data: {
- attributes: {
- fail: number;
- pass: number;
- muted: number;
- pass_new: number;
- fail_new: number;
- muted_new: number;
- total: number;
- };
- };
- };
-}
-
-const chartConfig = {
- number: {
- label: "Findings",
- },
- success: {
- label: "Success",
- color: "var(--color-bg-pass)",
- },
- fail: {
- label: "Fail",
- color: "var(--color-bg-fail)",
- },
- muted: {
- label: "Muted",
- color: "var(--color-bg-neutral-tertiary)",
- },
-} satisfies ChartConfig;
-
-export const FindingsByStatusChart: React.FC = ({
- findingsByStatus,
-}) => {
- const searchParams = useSearchParams();
- const shouldShowMuted = searchParams.get("filter[muted]") !== "false";
-
- const {
- fail = 0,
- pass = 0,
- muted = 0,
- pass_new = 0,
- fail_new = 0,
- muted_new = 0,
- } = findingsByStatus?.data?.attributes || {};
-
- const chartData = useMemo(() => {
- const data = [
- {
- findings: "Success",
- number: pass,
- fill: "var(--color-success)",
- },
- {
- findings: "Fail",
- number: fail,
- fill: "var(--color-fail)",
- },
- ];
-
- if (shouldShowMuted) {
- data.push({
- findings: "Muted",
- number: muted,
- fill: "var(--color-muted)",
- });
- }
-
- return data;
- }, [pass, fail, muted, shouldShowMuted]);
-
- const updatedChartData = calculatePercent(chartData);
-
- const totalFindings = useMemo(
- () => chartData.reduce((acc, curr) => acc + curr.number, 0),
- [chartData],
- );
-
- const hasDataToShow = totalFindings > 0;
-
- const emptyChartData = [
- {
- findings: "Empty",
- number: 1,
- fill: "hsl(var(--heroui-default-200))",
- },
- ];
-
- return (
-
-
-
-
-
- } />
-
- {
- if (viewBox && "cx" in viewBox && "cy" in viewBox) {
- return (
-
-
- {hasDataToShow
- ? totalFindings.toLocaleString()
- : "0"}
-
-
- {"Findings"}
-
-
- );
- }
- }}
- />
-
-
-
-
-
-
-
-
- }
- color="success"
- radius="lg"
- size="md"
- >
- {chartData[0].number}
-
- {updatedChartData[0].percent}
-
-
-
- {pass_new > 0 ? (
- <>
- +{pass_new} pass findings from last day{" "}
-
- >
- ) : pass_new < 0 ? (
- <>{pass_new} pass findings from last day>
- ) : (
- "No change from last day"
- )}
-
-
-
-
-
-
- }
- color="danger"
- radius="lg"
- size="md"
- >
- {chartData[1].number}
-
- {updatedChartData[1].percent}
-
-
-
- +{fail_new} fail findings from last day{" "}
-
-
-
-
-
- {shouldShowMuted ? (
- <>
-
-
- }
- color="warning"
- radius="lg"
- size="md"
- >
- {chartData.find((item) => item.findings === "Muted")
- ?.number || 0}
-
-
- {updatedChartData.find(
- (item) => item.findings === "Muted",
- )?.percent || "0%"}
-
-
-
-
- {muted_new > 0 ? (
- <>
- +{muted_new} muted findings from last day{" "}
-
- >
- ) : muted_new < 0 ? (
- <>{muted_new} muted findings from last day>
- ) : (
- "No change from last day"
- )}
-
- >
- ) : null}
-
-
-
-
-
- );
-};
diff --git a/ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx b/ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx
deleted file mode 100644
index 6565cebe22..0000000000
--- a/ui/components/overview/findings-by-status-chart/skeleton-findings-status-chart.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Card, CardBody, CardHeader } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
-
-export const SkeletonFindingsByStatusChart = () => {
- return (
-
-
-
-
-
-
-
-
- {/* Circle Chart Skeleton */}
-
-
-
-
- {/* Text Details Skeleton */}
-
- {/* Pass Findings */}
-
-
- {/* Fail Findings */}
-
-
-
-
-
- );
-};
diff --git a/ui/components/overview/index.ts b/ui/components/overview/index.ts
index e2beebb034..754f7a81d0 100644
--- a/ui/components/overview/index.ts
+++ b/ui/components/overview/index.ts
@@ -1,8 +1 @@
-export * from "./AttackSurface";
-export * from "./findings-by-severity-chart/findings-by-severity-chart";
-export * from "./findings-by-severity-chart/skeleton-findings-severity-chart";
-export * from "./findings-by-status-chart/findings-by-status-chart";
-export * from "./findings-by-status-chart/skeleton-findings-status-chart";
export * from "./new-findings-table/link-to-findings/link-to-findings";
-export * from "./provider-overview/provider-overview";
-export * from "./provider-overview/skeleton-provider-overview";
diff --git a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx
index e5766db634..74272133ce 100644
--- a/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx
+++ b/ui/components/overview/new-findings-table/link-to-findings/link-to-findings.tsx
@@ -1,19 +1,20 @@
"use client";
-import { CustomButton } from "@/components/ui/custom";
+import Link from "next/link";
+
+import { Button } from "@/components/shadcn/button/button";
export const LinkToFindings = () => {
return (
-
- Check out on Findings
-
+
+
+ Check out on Findings
+
+
);
};
diff --git a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx
index 59438f11da..103f6e6c03 100644
--- a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx
+++ b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx
@@ -53,7 +53,9 @@ const FindingDetailsCell = ({ row }: { row: any }) => {
return (
}
+ triggerComponent={
+
+ }
title="Finding Details"
description="View the finding details"
defaultOpen={isOpen}
diff --git a/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx b/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx
index c6ac03661b..7783e7ab2d 100644
--- a/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx
+++ b/ui/components/overview/new-findings-table/table/skeleton-table-new-findings.tsx
@@ -1,11 +1,39 @@
import React from "react";
-import { SkeletonTable } from "@/components/ui/skeleton/skeleton";
+import { Card } from "@/components/shadcn/card/card";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const SkeletonTableNewFindings = () => {
+ const columns = 7;
+ const rows = 3;
+
return (
-
-
-
+
+ {/* Table headers */}
+
+ {Array.from({ length: columns }).map((_, index) => (
+
+ ))}
+
+
+ {/* Table body */}
+
+ {Array.from({ length: rows }).map((_, rowIndex) => (
+
+ {Array.from({ length: columns }).map((_, colIndex) => (
+
+ ))}
+
+ ))}
+
+
);
};
diff --git a/ui/components/overview/provider-overview/provider-overview.tsx b/ui/components/overview/provider-overview/provider-overview.tsx
deleted file mode 100644
index e40dc25494..0000000000
--- a/ui/components/overview/provider-overview/provider-overview.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-"use client";
-
-import { Card, CardBody } from "@heroui/card";
-
-import { AddIcon } from "@/components/icons/Icons";
-import {
- AWSProviderBadge,
- AzureProviderBadge,
- GCPProviderBadge,
- GitHubProviderBadge,
- IacProviderBadge,
- KS8ProviderBadge,
- M365ProviderBadge,
- OracleCloudProviderBadge,
-} from "@/components/icons/providers-badge";
-import { CustomButton } from "@/components/ui/custom/custom-button";
-import { ProviderOverviewProps } from "@/types";
-import { PROVIDER_TYPES, ProviderType } from "@/types/providers";
-
-export const ProvidersOverview = ({
- providersOverview,
-}: {
- providersOverview: ProviderOverviewProps;
-}) => {
- const calculatePassingPercentage = (pass: number, total: number) =>
- total > 0 ? ((pass / total) * 100).toFixed(2) : "0.00";
-
- const renderProviderBadge = (providerId: ProviderType) => {
- switch (providerId) {
- case "aws":
- return
;
- case "azure":
- return
;
- case "m365":
- return
;
- case "gcp":
- return
;
- case "kubernetes":
- return
;
- case "github":
- return
;
- case "iac":
- return
;
- case "oraclecloud":
- return
;
- default:
- return null;
- }
- };
-
- const providerDisplayNames: Record
= {
- aws: "AWS",
- azure: "Azure",
- m365: "M365",
- gcp: "GCP",
- kubernetes: "Kubernetes",
- github: "GitHub",
- iac: "IaC",
- oraclecloud: "OCI",
- };
-
- const providers = PROVIDER_TYPES.map((providerType) => ({
- id: providerType,
- name: providerDisplayNames[providerType],
- }));
-
- if (!providersOverview || !Array.isArray(providersOverview.data)) {
- return (
-
-
-
-
- Provider
-
- Percent
- Passing
-
-
- Failing
- Checks
-
-
- Total
- Resources
-
-
-
- {providers.map((providerTemplate) => (
-
-
- {renderProviderBadge(providerTemplate.id)}
-
- 0.00%
- -
- -
-
- ))}
-
-
-
- Total
-
- 0.00%
- -
- -
-
-
-
-
- );
- }
-
- return (
-
-
-
-
- Provider
-
- Percent
- Passing
-
-
- Failing
- Checks
-
-
- Total
- Resources
-
-
-
- {providers.map((providerTemplate) => {
- const providerData = providersOverview.data.find(
- (p) => p.id === providerTemplate.id,
- );
-
- return (
-
-
- {renderProviderBadge(providerTemplate.id)}
-
-
- {providerData
- ? calculatePassingPercentage(
- providerData.attributes.findings.pass,
- providerData.attributes.findings.total,
- )
- : "0.00"}
- %
-
-
- {providerData ? providerData.attributes.findings.fail : "-"}
-
-
- {providerData ? providerData.attributes.resources.total : "-"}
-
-
- );
- })}
-
- {/* Totals row */}
-
- Total
-
- {calculatePassingPercentage(
- providersOverview.data.reduce(
- (sum, provider) => sum + provider.attributes.findings.pass,
- 0,
- ),
- providersOverview.data.reduce(
- (sum, provider) => sum + provider.attributes.findings.total,
- 0,
- ),
- )}
- %
-
-
- {providersOverview.data.reduce(
- (sum, provider) => sum + provider.attributes.findings.fail,
- 0,
- )}
-
-
- {providersOverview.data.reduce(
- (sum, provider) => sum + provider.attributes.resources.total,
- 0,
- )}
-
-
-
-
- }
- >
- Add Provider
-
-
-
-
- );
-};
diff --git a/ui/components/overview/provider-overview/skeleton-provider-overview.tsx b/ui/components/overview/provider-overview/skeleton-provider-overview.tsx
deleted file mode 100644
index 810c41c854..0000000000
--- a/ui/components/overview/provider-overview/skeleton-provider-overview.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Card, CardBody, CardHeader } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
-
-export const SkeletonProvidersOverview = () => {
- const rows = 4;
-
- return (
-
-
-
-
-
-
-
-
- {/* Header Skeleton */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Row Skeletons */}
- {Array.from({ length: rows }).map((_, index) => (
-
- {/* Provider Name */}
-
- {/* Percent Passing */}
-
-
-
- {/* Failing Checks */}
-
-
-
- {/* Total Resources */}
-
-
-
-
- ))}
-
-
-
- );
-};
diff --git a/ui/components/providers/add-provider-button.tsx b/ui/components/providers/add-provider-button.tsx
index 24b1b54c86..7846616357 100644
--- a/ui/components/providers/add-provider-button.tsx
+++ b/ui/components/providers/add-provider-button.tsx
@@ -1,19 +1,18 @@
"use client";
+import Link from "next/link";
+
+import { Button } from "@/components/shadcn";
+
import { AddIcon } from "../icons";
-import { CustomButton } from "../ui/custom";
export const AddProviderButton = () => {
return (
- }
- >
- Add Cloud Provider
-
+
+
+ Add Cloud Provider
+
+
+
);
};
diff --git a/ui/components/providers/enhanced-provider-selector.tsx b/ui/components/providers/enhanced-provider-selector.tsx
index 3282ede810..26f065be8e 100644
--- a/ui/components/providers/enhanced-provider-selector.tsx
+++ b/ui/components/providers/enhanced-provider-selector.tsx
@@ -1,12 +1,12 @@
"use client";
-import { Button } from "@heroui/button";
import { Input } from "@heroui/input";
import { Select, SelectItem } from "@heroui/select";
import { CheckSquare, Search, Square } from "lucide-react";
import { useMemo, useState } from "react";
import { Control } from "react-hook-form";
+import { Button } from "@/components/shadcn";
import { FormControl, FormField, FormMessage } from "@/components/ui/form";
import { ProviderProps, ProviderType } from "@/types/providers";
@@ -123,22 +123,20 @@ export const EnhancedProviderSelector = ({
{isMultiple && filteredProviders.length > 1 && (
-
+
{label}
- ) : (
-
- )
- }
+ variant="ghost"
+ onClick={handleSelectAll}
className="h-7 text-xs"
>
+ {isAllSelected ? (
+
+ ) : (
+
+ )}
{isAllSelected ? "Deselect All" : "Select All"}
@@ -158,12 +156,12 @@ export const EnhancedProviderSelector = ({
isInvalid={isInvalid}
classNames={{
trigger: "min-h-12",
- popoverContent: "dark:bg-gray-800",
- listboxWrapper: "max-h-[300px] dark:bg-gray-800",
+ popoverContent: "bg-bg-neutral-secondary",
+ listboxWrapper: "max-h-[300px] bg-bg-neutral-secondary",
listbox: "gap-0",
label:
- "tracking-tight font-light !text-default-500 text-xs z-0!",
- value: "text-default-500 text-small dark:text-gray-300",
+ "tracking-tight font-light !text-text-neutral-secondary text-xs z-0!",
+ value: "text-text-neutral-secondary text-small",
}}
renderValue={(items) => {
if (!isMultiple && value) {
@@ -214,7 +212,7 @@ export const EnhancedProviderSelector = ({
}}
listboxProps={{
topContent: enableSearch ? (
-
+
setSearchValue("")}
classNames={{
inputWrapper:
- "border-default-200 bg-transparent hover:bg-default-100/50 dark:bg-transparent dark:hover:bg-default-100/20",
+ "border-border-input-primary bg-bg-input-primary hover:bg-bg-neutral-secondary",
input: "text-small",
clearButton: "text-default-400",
}}
@@ -256,10 +254,10 @@ export const EnhancedProviderSelector = ({
{displayName}
-
+
{typeLabel}
{isDisabled && (
-
+
(Already used)
)}
@@ -270,8 +268,8 @@ export const EnhancedProviderSelector = ({
{showFormMessage && (
-
+
)}
>
);
diff --git a/ui/components/providers/forms/delete-form.tsx b/ui/components/providers/forms/delete-form.tsx
index d7b005640f..580d192641 100644
--- a/ui/components/providers/forms/delete-form.tsx
+++ b/ui/components/providers/forms/delete-form.tsx
@@ -7,8 +7,8 @@ import * as z from "zod";
import { deleteProvider } from "@/actions/providers";
import { DeleteIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import { CustomButton } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
@@ -59,33 +59,26 @@ export const DeleteForm = ({
name={ProviderCredentialFields.PROVIDER_ID}
value={providerId}
/>
-
-
+ setIsOpen(false)}
- isDisabled={isLoading}
+ onClick={() => setIsOpen(false)}
+ disabled={isLoading}
>
- Cancel
-
+ Cancel
+
- }
+ disabled={isLoading}
>
- {isLoading ? <>Loading> : Delete }
-
+ {!isLoading && }
+ {isLoading ? "Loading" : "Delete"}
+
diff --git a/ui/components/providers/forms/edit-form.tsx b/ui/components/providers/forms/edit-form.tsx
index 9e3b3de6d6..6ecab34368 100644
--- a/ui/components/providers/forms/edit-form.tsx
+++ b/ui/components/providers/forms/edit-form.tsx
@@ -6,10 +6,9 @@ import { useForm } from "react-hook-form";
import * as z from "zod";
import { updateProvider } from "@/actions/providers";
-import { SaveIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
-import { CustomButton, CustomInput } from "@/components/ui/custom";
-import { Form } from "@/components/ui/form";
+import { CustomInput } from "@/components/ui/custom";
+import { Form, FormButtons } from "@/components/ui/form";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { editProviderFormSchema } from "@/types";
@@ -82,40 +81,11 @@ export const EditForm = ({
placeholder={providerAlias}
variant="bordered"
isRequired={false}
- isInvalid={
- !!form.formState.errors[ProviderCredentialFields.PROVIDER_ALIAS]
- }
/>
-
- setIsOpen(false)}
- isDisabled={isLoading}
- >
- Cancel
-
-
- }
- >
- {isLoading ? <>Loading> : Save }
-
-
+
);
diff --git a/ui/components/providers/forms/muted-findings-config-form.tsx b/ui/components/providers/forms/muted-findings-config-form.tsx
index b52a040520..678ae9ee9c 100644
--- a/ui/components/providers/forms/muted-findings-config-form.tsx
+++ b/ui/components/providers/forms/muted-findings-config-form.tsx
@@ -16,8 +16,8 @@ import {
updateMutedFindingsConfig,
} from "@/actions/processors";
import { DeleteIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { FormButtons } from "@/components/ui/form";
import { fontMono } from "@/config/fonts";
@@ -142,30 +142,35 @@ export const MutedFindingsConfigForm = ({
be undone.
- setShowDeleteConfirmation(false)}
- isDisabled={isDeleting}
+ onClick={() => setShowDeleteConfirmation(false)}
+ disabled={isDeleting}
>
Cancel
-
-
+ }
- onPress={handleDelete}
+ disabled={isDeleting}
+ onClick={handleDelete}
>
- {isDeleting ? "Deleting" : "Delete"}
-
+ {isDeleting ? (
+ "Deleting"
+ ) : (
+ <>
+
+ Delete
+ >
+ )}
+
);
@@ -250,19 +255,18 @@ export const MutedFindingsConfigForm = ({
/>
{config && (
-
}
- onPress={() => setShowDeleteConfirmation(true)}
- isDisabled={isPending}
+ variant="outline"
+ size="default"
+ onClick={() => setShowDeleteConfirmation(true)}
+ disabled={isPending}
>
+
Delete Configuration
-
+
)}
diff --git a/ui/components/providers/link-to-scans.tsx b/ui/components/providers/link-to-scans.tsx
index bcd694cbe0..81fd9816b8 100644
--- a/ui/components/providers/link-to-scans.tsx
+++ b/ui/components/providers/link-to-scans.tsx
@@ -1,6 +1,8 @@
"use client";
-import { CustomButton } from "@/components/ui/custom";
+import Link from "next/link";
+
+import { Button } from "@/components/shadcn";
interface LinkToScansProps {
providerUid?: string;
@@ -8,14 +10,10 @@ interface LinkToScansProps {
export const LinkToScans = ({ providerUid }: LinkToScansProps) => {
return (
-
- View Scan Jobs
-
+
+
+ View Scan Jobs
+
+
);
};
diff --git a/ui/components/providers/muted-findings-config-button.tsx b/ui/components/providers/muted-findings-config-button.tsx
index c560222f2d..2508c3ad63 100644
--- a/ui/components/providers/muted-findings-config-button.tsx
+++ b/ui/components/providers/muted-findings-config-button.tsx
@@ -4,7 +4,8 @@ import { SettingsIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
-import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
+import { Button } from "@/components/shadcn";
+import { CustomAlertModal } from "@/components/ui/custom";
import { useUIStore } from "@/store/ui/store";
import { MutedFindingsConfigForm } from "./forms";
@@ -62,17 +63,14 @@ export const MutedFindingsConfigButton = () => {
/>
-
}
- onPress={handleOpenModal}
- isDisabled={!hasProviders}
+
+
Configure Mutelist
-
+
>
);
};
diff --git a/ui/components/providers/provider-info.tsx b/ui/components/providers/provider-info.tsx
index 3e7de97bc8..136ac01b96 100644
--- a/ui/components/providers/provider-info.tsx
+++ b/ui/components/providers/provider-info.tsx
@@ -32,7 +32,7 @@ export const ProviderInfo: React.FC
= ({
return (
-
+
);
diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx
index 3b04cba457..8d40d8c910 100644
--- a/ui/components/providers/radio-group-provider.tsx
+++ b/ui/components/providers/radio-group-provider.tsx
@@ -98,7 +98,7 @@ export const RadioGroupProvider: React.FC = ({
{errorMessage && (
-
+
{errorMessage}
)}
diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx
index 9dd21b0b45..1c47d2feee 100644
--- a/ui/components/providers/table/data-table-row-actions.tsx
+++ b/ui/components/providers/table/data-table-row-actions.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Button } from "@heroui/button";
import {
Dropdown,
DropdownItem,
@@ -20,6 +19,7 @@ import { useState } from "react";
import { checkConnectionProvider } from "@/actions/providers/providers";
import { VerticalDotsIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { CustomAlertModal } from "@/components/ui/custom";
import { EditForm } from "../forms";
@@ -83,12 +83,12 @@ export function DataTableRowActions({
-
-
+
+
({
}
onPress={() => setIsDeleteOpen(true)}
diff --git a/ui/components/providers/table/skeleton-table-provider.tsx b/ui/components/providers/table/skeleton-table-provider.tsx
index 667feaf84f..4476919840 100644
--- a/ui/components/providers/table/skeleton-table-provider.tsx
+++ b/ui/components/providers/table/skeleton-table-provider.tsx
@@ -1,33 +1,20 @@
-import { Card } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
import React from "react";
+import { Card } from "@/components/shadcn/card/card";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
+
export const SkeletonTableProviders = () => {
return (
-
+
{/* Table headers */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{/* Table body */}
@@ -35,29 +22,15 @@ export const SkeletonTableProviders = () => {
{[...Array(3)].map((_, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
))}
diff --git a/ui/components/providers/workflow/credentials-role-helper.tsx b/ui/components/providers/workflow/credentials-role-helper.tsx
index 10757b3256..46554fbacf 100644
--- a/ui/components/providers/workflow/credentials-role-helper.tsx
+++ b/ui/components/providers/workflow/credentials-role-helper.tsx
@@ -1,7 +1,7 @@
"use client";
import { IdIcon } from "@/components/icons";
-import { CustomButton } from "@/components/ui/custom";
+import { Button } from "@/components/shadcn";
import { SnippetChip } from "@/components/ui/entities";
import { IntegrationType } from "@/types/integrations";
@@ -30,15 +30,21 @@ export const CredentialsRoleHelper = ({
{isAmazonS3 ? " or updated" : ""}
-
- Use the following AWS CloudFormation Quick Link to create the IAM Role
-
+
+ Use the following AWS CloudFormation Quick Link to create the IAM
+ Role
+
+
@@ -55,24 +61,34 @@ export const CredentialsRoleHelper = ({
diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx
index d7835386c9..9bc6d59148 100644
--- a/ui/components/providers/workflow/forms/base-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx
@@ -1,10 +1,10 @@
"use client";
import { Divider } from "@heroui/divider";
-import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { ChevronLeftIcon, ChevronRightIcon, Loader2 } from "lucide-react";
import { Control } from "react-hook-form";
-import { CustomButton } from "@/components/ui/custom";
+import { Button } from "@/components/shadcn";
import { Form } from "@/components/ui/form";
import { useCredentialsForm } from "@/hooks/use-credentials-form";
import { getAWSCredentialsTemplateLinks } from "@/lib";
@@ -173,43 +173,32 @@ export const BaseCredentialsForm = ({
/>
)}
-
+
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && (
- }
- isDisabled={isLoading}
+ onClick={handleBackStep}
+ disabled={isLoading}
>
- Back
-
+ {!isLoading && }
+ Back
+
)}
- }
- onPress={(e) => {
- const formElement = e.target as HTMLElement;
- const form = formElement.closest("form");
- if (form) {
- form.dispatchEvent(
- new Event("submit", { bubbles: true, cancelable: true }),
- );
- }
- }}
+ disabled={isLoading}
>
- {isLoading ? <>Loading> : {submitButtonText} }
-
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ {isLoading ? "Loading" : submitButtonText}
+
diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx
index f0ede535bf..e91e9fb4a4 100644
--- a/ui/components/providers/workflow/forms/connect-account-form.tsx
+++ b/ui/components/providers/workflow/forms/connect-account-form.tsx
@@ -1,7 +1,7 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
-import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import { ChevronLeftIcon, ChevronRightIcon, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -9,8 +9,9 @@ import * as z from "zod";
import { addProvider } from "@/actions/providers/providers";
import { ProviderTitleDocs } from "@/components/providers/workflow/provider-title-docs";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { addProviderFormSchema, ApiError, ProviderType } from "@/types";
@@ -200,7 +201,6 @@ export const ConnectAccountForm = () => {
placeholder={providerFieldDetails.placeholder}
variant="bordered"
isRequired
- isInvalid={!!form.formState.errors.providerUid}
/>
{
placeholder="Enter the provider alias"
variant="bordered"
isRequired={false}
- isInvalid={!!form.formState.errors.providerAlias}
/>
>
)}
{/* Navigation buttons */}
-
+
{/* Show "Back" button only in Step 2 */}
{prevStep === 2 && (
- }
- isDisabled={isLoading}
+ onClick={handleBackStep}
+ disabled={isLoading}
>
- Back
-
+ {!isLoading && }
+ Back
+
)}
{/* Show "Next" button in Step 2 */}
{prevStep === 2 && (
- }
+ disabled={isLoading}
>
- {isLoading ? <>Loading> : Next }
-
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ {isLoading ? "Loading" : "Next"}
+
)}
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx
index 30f5260fe9..ad38a761c0 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx
@@ -130,11 +130,6 @@ export const AWSRoleCredentialsForm = ({
placeholder="Enter the AWS Access Key ID"
variant="bordered"
isRequired
- isInvalid={
- !!control._formState.errors[
- ProviderCredentialFields.AWS_ACCESS_KEY_ID
- ]
- }
/>
>
)}
@@ -207,9 +192,6 @@ export const AWSRoleCredentialsForm = ({
placeholder="Enter the Role ARN"
variant="bordered"
isRequired={showRoleSection}
- isInvalid={
- !!control._formState.errors[ProviderCredentialFields.ROLE_ARN]
- }
/>
Optional fields
@@ -238,11 +217,6 @@ export const AWSRoleCredentialsForm = ({
placeholder="Enter the role session name"
variant="bordered"
isRequired={false}
- isInvalid={
- !!control._formState.errors[
- ProviderCredentialFields.ROLE_SESSION_NAME
- ]
- }
/>
>
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-static-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-static-credentials-form.tsx
index 1a8fb3480f..429f12aec8 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-static-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-static-credentials-form.tsx
@@ -28,11 +28,6 @@ export const AWSStaticCredentialsForm = ({
placeholder="Enter the AWS Access Key ID"
variant="bordered"
isRequired
- isInvalid={
- !!control._formState.errors[
- ProviderCredentialFields.AWS_ACCESS_KEY_ID
- ]
- }
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx
index eda607b469..7fbcd62339 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/radio-group-aws-via-credentials-type-form.tsx
@@ -59,7 +59,7 @@ export const RadioGroupAWSViaCredentialsTypeForm = ({
{errorMessage && (
-
+
{errorMessage}
)}
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-default-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-default-credentials-form.tsx
index 9c2b259e93..4c76467e8d 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-default-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-default-credentials-form.tsx
@@ -27,7 +27,6 @@ export const GCPDefaultCredentialsForm = ({
placeholder="Enter the Client ID"
variant="bordered"
isRequired
- isInvalid={!!control._formState.errors.client_id}
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-service-account-key-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-service-account-key-form.tsx
index f83ae390d5..a53e1739af 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-service-account-key-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-service-account-key-form.tsx
@@ -27,7 +27,6 @@ export const GCPServiceAccountKeyForm = ({
variant="bordered"
minRows={10}
isRequired
- isInvalid={!!control._formState.errors.service_account_key}
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx
index 75036fbb85..147e5df048 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/gcp/radio-group-gcp-via-credentials-type-form.tsx
@@ -66,7 +66,7 @@ export const RadioGroupGCPViaCredentialsTypeForm = ({
{errorMessage && (
-
+
{errorMessage}
)}
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx
index 0f06fa6c43..dabf7df380 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-app-form.tsx
@@ -25,9 +25,6 @@ export const GitHubAppForm = ({ control }: { control: Control }) => {
placeholder="Enter your GitHub App ID"
variant="bordered"
isRequired
- isInvalid={
- !!control._formState.errors[ProviderCredentialFields.GITHUB_APP_ID]
- }
/>
}) => {
variant="bordered"
isRequired
minRows={4}
- isInvalid={
- !!control._formState.errors[ProviderCredentialFields.GITHUB_APP_KEY]
- }
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx
index bc3e882c80..3fe19d6119 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-oauth-app-form.tsx
@@ -25,9 +25,6 @@ export const GitHubOAuthAppForm = ({ control }: { control: Control }) => {
placeholder="Enter your GitHub OAuth App token"
variant="bordered"
isRequired
- isInvalid={
- !!control._formState.errors[ProviderCredentialFields.OAUTH_APP_TOKEN]
- }
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx
index 6d0e5bba66..e07b1d8f16 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/github/credentials-type/github-personal-access-token-form.tsx
@@ -29,11 +29,6 @@ export const GitHubPersonalAccessTokenForm = ({
placeholder="Enter your GitHub personal access token"
variant="bordered"
isRequired
- isInvalid={
- !!control._formState.errors[
- ProviderCredentialFields.PERSONAL_ACCESS_TOKEN
- ]
- }
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx
index 9c32c3c9f0..1e806ecc45 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/github/radio-group-github-via-credentials-type-form.tsx
@@ -73,7 +73,7 @@ export const RadioGroupGitHubViaCredentialsTypeForm = ({
{errorMessage && (
-
+
{errorMessage}
)}
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx
index 03dd5a72e0..1b07ae3129 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx
@@ -31,7 +31,6 @@ export const M365CertificateCredentialsForm = ({
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
- isInvalid={!!control._formState.errors.tenant_id}
/>
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx
index 3312fb536a..3393fbdff5 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx
@@ -29,7 +29,6 @@ export const M365ClientSecretCredentialsForm = ({
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
- isInvalid={!!control._formState.errors.tenant_id}
/>
>
);
diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx
index 2f5599363b..c1560e90c2 100644
--- a/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx
+++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx
@@ -61,7 +61,7 @@ export const RadioGroupM365ViaCredentialsTypeForm = ({
{errorMessage && (
-
+
{errorMessage}
)}
diff --git a/ui/components/providers/workflow/forms/test-connection-form.tsx b/ui/components/providers/workflow/forms/test-connection-form.tsx
index d897508b00..0042f5df2f 100644
--- a/ui/components/providers/workflow/forms/test-connection-form.tsx
+++ b/ui/components/providers/workflow/forms/test-connection-form.tsx
@@ -3,6 +3,7 @@
import { Checkbox } from "@heroui/checkbox";
import { zodResolver } from "@hookform/resolvers/zod";
import { Icon } from "@iconify/react";
+import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -15,8 +16,8 @@ import {
import { scanOnDemand, scheduleDaily } from "@/actions/scans";
import { getTask } from "@/actions/task/tasks";
import { CheckIcon, RocketIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { Form } from "@/components/ui/form";
import { checkTaskStatus } from "@/lib/helper";
@@ -238,7 +239,7 @@ export const TestConnectionForm = ({
{apiErrorMessage && (
-
+
{`Provider ID ${apiErrorMessage?.toLowerCase()}. Please check and try again.`}
)}
@@ -249,16 +250,16 @@ export const TestConnectionForm = ({
-
+
{connectionStatus.error || "Unknown error"}
-
+
It seems there was an issue with your credentials. Please review
your credentials and try again.
@@ -277,9 +278,10 @@ export const TestConnectionForm = ({
{...form.register("runOnce")}
isSelected={!!form.watch("runOnce")}
classNames={{
- label: "text-small text-default-500",
+ label: "text-small",
wrapper: "checkbox-update",
}}
+ color="default"
>
Run a single scan (no recurring schedule).
@@ -307,45 +309,44 @@ export const TestConnectionForm = ({
Back to providers
) : connectionStatus?.error ? (
-
router.back() : onResetCredentials}
+ router.back() : onResetCredentials}
type="button"
- ariaLabel={"Save"}
- className="w-1/2"
- variant="solid"
- color="warning"
- size="md"
- isLoading={isResettingCredentials}
- startContent={!isResettingCredentials && }
- isDisabled={isResettingCredentials}
+ variant="secondary"
+ size="lg"
+ disabled={isResettingCredentials}
>
{isResettingCredentials ? (
- <>Loading>
+
) : (
-
- {isUpdated ? "Update credentials" : "Reset credentials"}
-
+
)}
-
+ {isResettingCredentials
+ ? "Loading"
+ : isUpdated
+ ? "Update credentials"
+ : "Reset credentials"}
+
) : (
-
}
+ variant="default"
+ size="lg"
+ disabled={isLoading}
>
{isLoading ? (
- <>Loading>
+
) : (
-
{isUpdated ? "Check connection" : "Launch scan"}
+ !isUpdated &&
)}
-
+ {isLoading
+ ? "Loading"
+ : isUpdated
+ ? "Check connection"
+ : "Launch scan"}
+
)}
diff --git a/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
index c911dd66c4..b09e74160e 100644
--- a/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
@@ -27,7 +27,6 @@ export const AzureCredentialsForm = ({
placeholder="Enter the Client ID"
variant="bordered"
isRequired
- isInvalid={!!control._formState.errors.client_id}
/>
>
);
diff --git a/ui/components/providers/workflow/forms/via-credentials/iac-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/iac-credentials-form.tsx
index e44ea30a1c..7714e86a52 100644
--- a/ui/components/providers/workflow/forms/via-credentials/iac-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/via-credentials/iac-credentials-form.tsx
@@ -26,7 +26,7 @@ export const IacCredentialsForm = ({
placeholder="Token for private repositories (optional)"
variant="bordered"
type="password"
- isInvalid={!!control._formState.errors.access_token}
+ isRequired={false}
/>
>
);
diff --git a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
index 51619fcfbb..c653d43952 100644
--- a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
@@ -27,7 +27,6 @@ export const KubernetesCredentialsForm = ({
variant="bordered"
minRows={10}
isRequired
- isInvalid={!!control._formState.errors.kubeconfig_content}
/>
>
);
diff --git a/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx
index 54042fa7db..1689ebabba 100644
--- a/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx
+++ b/ui/components/providers/workflow/forms/via-credentials/oraclecloud-credentials-form.tsx
@@ -34,7 +34,6 @@ export const OracleCloudCredentialsForm = ({
placeholder="ocid1.user.oc1..aaaaaaa..."
variant="bordered"
isRequired
- isInvalid={!!control._formState.errors.user}
/>
Paste the raw content of your OCI private key file (PEM format). The key
diff --git a/ui/components/providers/workflow/provider-title-docs.tsx b/ui/components/providers/workflow/provider-title-docs.tsx
index ae2f0d6d0f..51081f17ce 100644
--- a/ui/components/providers/workflow/provider-title-docs.tsx
+++ b/ui/components/providers/workflow/provider-title-docs.tsx
@@ -28,6 +28,7 @@ export const ProviderTitleDocs = ({
Read the docs
diff --git a/ui/components/providers/workflow/vertical-steps.tsx b/ui/components/providers/workflow/vertical-steps.tsx
index 405d33f631..08d1f31ea9 100644
--- a/ui/components/providers/workflow/vertical-steps.tsx
+++ b/ui/components/providers/workflow/vertical-steps.tsx
@@ -1,6 +1,5 @@
"use client";
-import type { ButtonProps } from "@heroui/button";
import { cn } from "@heroui/theme";
import { useControlledState } from "@react-stately/utils";
import { domAnimation, LazyMotion, m } from "framer-motion";
@@ -26,7 +25,13 @@ export interface VerticalStepsProps
*
* @default "primary"
*/
- color?: ButtonProps["color"];
+ color?:
+ | "primary"
+ | "secondary"
+ | "success"
+ | "warning"
+ | "danger"
+ | "default";
/**
* The current step index.
*/
@@ -209,13 +214,12 @@ export const VerticalSteps = React.forwardRef<
},
active: {
backgroundColor: "transparent",
- borderColor: "var(--active-border-color)",
- color: "var(--active-color)",
+ borderColor: "var(--bg-button-primary)",
+ color: "var(--bg-button-primary)",
},
complete: {
- backgroundColor:
- "var(--complete-background-color)",
- borderColor: "var(--complete-border-color)",
+ backgroundColor: "var(--bg-button-primary)",
+ borderColor: "var(--bg-button-primary)",
},
}}
>
diff --git a/ui/components/providers/workflow/workflow-add-provider.tsx b/ui/components/providers/workflow/workflow-add-provider.tsx
index 28aae0b25c..fbb5a3e393 100644
--- a/ui/components/providers/workflow/workflow-add-provider.tsx
+++ b/ui/components/providers/workflow/workflow-add-provider.tsx
@@ -74,7 +74,8 @@ export const WorkflowAddProvider = () => {
classNames={{
base: "px-0.5 mb-5",
label: "text-small",
- value: "text-small text-default-400",
+ value: "text-small text-button-primary",
+ indicator: "bg-button-primary",
}}
label="Steps"
maxValue={steps.length - 1}
@@ -87,7 +88,7 @@ export const WorkflowAddProvider = () => {
diff --git a/ui/components/resources/skeleton/skeleton-table-resources.tsx b/ui/components/resources/skeleton/skeleton-table-resources.tsx
index fb99a22a72..4fa4b024ea 100644
--- a/ui/components/resources/skeleton/skeleton-table-resources.tsx
+++ b/ui/components/resources/skeleton/skeleton-table-resources.tsx
@@ -1,33 +1,20 @@
-import { Card } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
import React from "react";
+import { Card } from "@/components/shadcn/card/card";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
+
export const SkeletonTableResources = () => {
return (
-
+
{/* Table headers */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{/* Table body */}
@@ -35,29 +22,15 @@ export const SkeletonTableResources = () => {
{[...Array(3)].map((_, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
))}
diff --git a/ui/components/resources/table/column-resources.tsx b/ui/components/resources/table/column-resources.tsx
index dd8c7ca08e..6739f17103 100644
--- a/ui/components/resources/table/column-resources.tsx
+++ b/ui/components/resources/table/column-resources.tsx
@@ -43,7 +43,9 @@ const ResourceDetailsCell = ({ row }: { row: any }) => {
return (
}
+ triggerComponent={
+
+ }
title="Resource Details"
description="View the Resource details"
defaultOpen={isOpen}
diff --git a/ui/components/resources/table/resource-detail.tsx b/ui/components/resources/table/resource-detail.tsx
index e8e118cb91..f042fa593d 100644
--- a/ui/components/resources/table/resource-detail.tsx
+++ b/ui/components/resources/table/resource-detail.tsx
@@ -9,11 +9,11 @@ import { useEffect, useState } from "react";
import { getFindingById } from "@/actions/findings";
import { getResourceById } from "@/actions/resources";
import { FindingDetail } from "@/components/findings/table/finding-detail";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { BreadcrumbNavigation, CustomBreadcrumbItem } from "@/components/ui";
-import { CustomSection } from "@/components/ui/custom";
import {
DateWithTime,
- EntityInfoShort,
+ getProviderLogo,
InfoField,
} from "@/components/ui/entities";
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
@@ -194,150 +194,150 @@ export const ResourceDetail = ({
return (
{/* Resource Details section */}
-
- Resource Details
- {gitUrl && (
-
-
-
-
-
- )}
-
- ) : (
- "Resource Details"
- )
- }
- >
-
+
+
+
+
Resource Details
+ {providerData.provider === "iac" && gitUrl && (
+
+
+
+
+
+ )}
+
+ {getProviderLogo(providerData.provider as ProviderType)}
+
+
-
+
{renderValue(attributes.uid)}
-
-
+
+
+
+ {renderValue(attributes.name)}
+
+
+ {renderValue(attributes.type)}
+
+
+
+
+ {renderValue(attributes.service)}
+
+
+ {renderValue(attributes.region)}
+
+
+
+
+
+
+
+
+
-
-
-
- {renderValue(attributes.name)}
-
-
- {renderValue(attributes.type)}
-
-
-
-
- {renderValue(attributes.service)}
-
- {renderValue(attributes.region)}
-
-
-
-
-
-
-
-
-
-
- {resourceTags && Object.entries(resourceTags).length > 0 ? (
-
-
- Tags
-
-
- {Object.entries(resourceTags).map(([key, value]) => (
-
- {renderValue(value)}
-
- ))}
+ {resourceTags && Object.entries(resourceTags).length > 0 ? (
+
+
+ Tags
+
+
+ {Object.entries(resourceTags).map(([key, value]) => (
+
+ {renderValue(value)}
+
+ ))}
+
-
- ) : null}
-
+ ) : null}
+
+
{/* Finding associated with this resource section */}
-
- {findingsLoading ? (
-
-
-
- Loading findings...
-
-
- ) : allFindings.length > 0 ? (
-
-
- Total findings: {allFindings.length}
-
- {allFindings.map((finding: any, index: number) => {
- const { attributes: findingAttrs, id } = finding;
+
+
+ Findings associated with this resource
+
+
+ {findingsLoading ? (
+
+
+
+ Loading findings...
+
+
+ ) : allFindings.length > 0 ? (
+
+
+ Total findings: {allFindings.length}
+
+ {allFindings.map((finding: any, index: number) => {
+ const { attributes: findingAttrs, id } = finding;
- // Handle cases where finding might not have all attributes
- if (!findingAttrs) {
- return (
-
-
- Finding {id} - No attributes available
-
-
- );
- }
-
- const { severity, check_metadata, status } = findingAttrs;
- const checktitle = check_metadata?.checktitle || "Unknown check";
-
- return (
-
navigateToFinding(id)}
- className="shadow-small dark:bg-prowler-blue-400 flex w-full cursor-pointer flex-col gap-2 rounded-lg px-4 py-2"
- >
-
-
- {checktitle}
-
-
-
-
-
navigateToFinding(id)}
- />
+ // Handle cases where finding might not have all attributes
+ if (!findingAttrs) {
+ return (
+
+
+ Finding {id} - No attributes available
+
-
-
- );
- })}
-
- ) : (
-
- No findings found for this resource.
-
- )}
-
+ );
+ }
+
+ const { severity, check_metadata, status } = findingAttrs;
+ const checktitle =
+ check_metadata?.checktitle || "Unknown check";
+
+ return (
+ navigateToFinding(id)}
+ className="shadow-small border-border-neutral-tertiary bg-bg-neutral-tertiary flex w-full cursor-pointer flex-col gap-2 rounded-lg px-4 py-2"
+ >
+
+
+ {checktitle}
+
+
+
+
+ navigateToFinding(id)}
+ />
+
+
+
+ );
+ })}
+
+ ) : (
+
+ No findings found for this resource.
+
+ )}
+
+
);
};
diff --git a/ui/components/roles/add-role-button.tsx b/ui/components/roles/add-role-button.tsx
deleted file mode 100644
index 3ac3b67126..0000000000
--- a/ui/components/roles/add-role-button.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-"use client";
-
-import { AddIcon } from "../icons";
-import { CustomButton } from "../ui/custom";
-
-export const AddRoleButton = () => {
- return (
-
- }
- >
- Add Role
-
-
- );
-};
diff --git a/ui/components/roles/index.ts b/ui/components/roles/index.ts
index 27454e73fd..4ae5064239 100644
--- a/ui/components/roles/index.ts
+++ b/ui/components/roles/index.ts
@@ -1 +1 @@
-export * from "./add-role-button";
+// Roles exports
diff --git a/ui/components/roles/table/data-table-row-actions.tsx b/ui/components/roles/table/data-table-row-actions.tsx
index 95be49f7ed..7e6f0e94ff 100644
--- a/ui/components/roles/table/data-table-row-actions.tsx
+++ b/ui/components/roles/table/data-table-row-actions.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Button } from "@heroui/button";
import {
Dropdown,
DropdownItem,
@@ -18,6 +17,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { VerticalDotsIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { CustomAlertModal } from "@/components/ui/custom/custom-alert-modal";
import { DeleteRoleForm } from "../workflow/forms";
@@ -44,12 +44,12 @@ export function DataTableRowActions({
-
-
+
+
({
}
onPress={() => setIsDeleteOpen(true)}
diff --git a/ui/components/roles/table/skeleton-table-roles.tsx b/ui/components/roles/table/skeleton-table-roles.tsx
index b5396cd888..15372fdc91 100644
--- a/ui/components/roles/table/skeleton-table-roles.tsx
+++ b/ui/components/roles/table/skeleton-table-roles.tsx
@@ -1,30 +1,19 @@
-import { Card } from "@heroui/card";
-import { Skeleton } from "@heroui/skeleton";
import React from "react";
+import { Card } from "@/components/shadcn/card/card";
+import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
+
export const SkeletonTableRoles = () => {
return (
-
+
{/* Table headers */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
{/* Table body */}
@@ -32,26 +21,14 @@ export const SkeletonTableRoles = () => {
{[...Array(10)].map((_, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
))}
diff --git a/ui/components/roles/workflow/forms/add-role-form.tsx b/ui/components/roles/workflow/forms/add-role-form.tsx
index 66a266226c..56c61308fd 100644
--- a/ui/components/roles/workflow/forms/add-role-form.tsx
+++ b/ui/components/roles/workflow/forms/add-role-form.tsx
@@ -5,7 +5,7 @@ import { Divider } from "@heroui/divider";
import { Tooltip } from "@heroui/tooltip";
import { zodResolver } from "@hookform/resolvers/zod";
import clsx from "clsx";
-import { InfoIcon, SaveIcon } from "lucide-react";
+import { InfoIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
@@ -13,12 +13,8 @@ import { z } from "zod";
import { addRole } from "@/actions/roles/roles";
import { useToast } from "@/components/ui";
-import {
- CustomButton,
- CustomDropdownSelection,
- CustomInput,
-} from "@/components/ui/custom";
-import { Form } from "@/components/ui/form";
+import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom";
+import { Form, FormButtons } from "@/components/ui/form";
import { getErrorMessage, permissionFormFields } from "@/lib";
import { addRoleFormSchema, ApiError } from "@/types";
@@ -162,7 +158,6 @@ export const AddRoleForm = ({
placeholder="Enter role name"
variant="bordered"
isRequired
- isInvalid={!!form.formState.errors.name}
/>
@@ -178,6 +173,7 @@ export const AddRoleForm = ({
label: "text-small",
wrapper: "checkbox-update",
}}
+ color="default"
>
Grant all admin permissions
@@ -199,6 +195,7 @@ export const AddRoleForm = ({
label: "text-small",
wrapper: "checkbox-update",
}}
+ color="default"
>
{label}
@@ -253,20 +250,7 @@ export const AddRoleForm = ({
)}
)}
-
- }
- >
- {isLoading ? <>Loading> : Add Role }
-
-
+
);
diff --git a/ui/components/roles/workflow/forms/delete-role-form.tsx b/ui/components/roles/workflow/forms/delete-role-form.tsx
index c6817734e9..94c1f7b9c3 100644
--- a/ui/components/roles/workflow/forms/delete-role-form.tsx
+++ b/ui/components/roles/workflow/forms/delete-role-form.tsx
@@ -7,8 +7,8 @@ import * as z from "zod";
import { deleteRole } from "@/actions/roles";
import { DeleteIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
-import { CustomButton } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
const formSchema = z.object({
@@ -54,33 +54,26 @@ export const DeleteRoleForm = ({
diff --git a/ui/components/roles/workflow/forms/edit-role-form.tsx b/ui/components/roles/workflow/forms/edit-role-form.tsx
index 9f5c279657..ab8fc5b567 100644
--- a/ui/components/roles/workflow/forms/edit-role-form.tsx
+++ b/ui/components/roles/workflow/forms/edit-role-form.tsx
@@ -5,7 +5,7 @@ import { Divider } from "@heroui/divider";
import { Tooltip } from "@heroui/tooltip";
import { zodResolver } from "@hookform/resolvers/zod";
import { clsx } from "clsx";
-import { InfoIcon, SaveIcon } from "lucide-react";
+import { InfoIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
@@ -13,12 +13,8 @@ import { z } from "zod";
import { updateRole } from "@/actions/roles/roles";
import { useToast } from "@/components/ui";
-import {
- CustomButton,
- CustomDropdownSelection,
- CustomInput,
-} from "@/components/ui/custom";
-import { Form } from "@/components/ui/form";
+import { CustomDropdownSelection, CustomInput } from "@/components/ui/custom";
+import { Form, FormButtons } from "@/components/ui/form";
import { getErrorMessage, permissionFormFields } from "@/lib";
import { ApiError, editRoleFormSchema } from "@/types";
@@ -182,7 +178,6 @@ export const EditRoleForm = ({
placeholder="Enter role name"
variant="bordered"
isRequired
- isInvalid={!!form.formState.errors.name}
/>
@@ -198,6 +193,7 @@ export const EditRoleForm = ({
label: "text-small",
wrapper: "checkbox-update",
}}
+ color="default"
>
Grant all admin permissions
@@ -219,6 +215,7 @@ export const EditRoleForm = ({
label: "text-small",
wrapper: "checkbox-update",
}}
+ color="default"
>
{label}
@@ -272,20 +269,7 @@ export const EditRoleForm = ({
)}
)}
-
- }
- >
- {isLoading ? <>Loading> : Update Role }
-
-
+
);
diff --git a/ui/components/roles/workflow/vertical-steps.tsx b/ui/components/roles/workflow/vertical-steps.tsx
index 405d33f631..02e6d8642f 100644
--- a/ui/components/roles/workflow/vertical-steps.tsx
+++ b/ui/components/roles/workflow/vertical-steps.tsx
@@ -1,6 +1,5 @@
"use client";
-import type { ButtonProps } from "@heroui/button";
import { cn } from "@heroui/theme";
import { useControlledState } from "@react-stately/utils";
import { domAnimation, LazyMotion, m } from "framer-motion";
@@ -26,7 +25,13 @@ export interface VerticalStepsProps
*
* @default "primary"
*/
- color?: ButtonProps["color"];
+ color?:
+ | "primary"
+ | "secondary"
+ | "success"
+ | "warning"
+ | "danger"
+ | "default";
/**
* The current step index.
*/
diff --git a/ui/components/roles/workflow/workflow-add-edit-role.tsx b/ui/components/roles/workflow/workflow-add-edit-role.tsx
index 628ea1c91c..201203c343 100644
--- a/ui/components/roles/workflow/workflow-add-edit-role.tsx
+++ b/ui/components/roles/workflow/workflow-add-edit-role.tsx
@@ -56,7 +56,7 @@ export const WorkflowAddEditRole = () => {
diff --git a/ui/components/scans/forms/edit-scan-form.tsx b/ui/components/scans/forms/edit-scan-form.tsx
index e7eab17a71..bb05b921a2 100644
--- a/ui/components/scans/forms/edit-scan-form.tsx
+++ b/ui/components/scans/forms/edit-scan-form.tsx
@@ -6,10 +6,9 @@ import { useForm } from "react-hook-form";
import * as z from "zod";
import { updateScan } from "@/actions/scans";
-import { SaveIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
-import { CustomButton, CustomInput } from "@/components/ui/custom";
-import { Form } from "@/components/ui/form";
+import { CustomInput } from "@/components/ui/custom";
+import { Form, FormButtons } from "@/components/ui/form";
import { editScanFormSchema } from "@/types";
export const EditScanForm = ({
@@ -82,38 +81,11 @@ export const EditScanForm = ({
placeholder={scanName || "Enter scan name"}
variant="bordered"
isRequired={false}
- isInvalid={!!form.formState.errors.scanName}
/>
-
- setIsOpen(false)}
- isDisabled={isLoading}
- >
- Cancel
-
-
- }
- >
- {isLoading ? <>Loading> : Save }
-
-
+
);
diff --git a/ui/components/scans/forms/schedule-form.tsx b/ui/components/scans/forms/schedule-form.tsx
index f75d4c50c4..5a0e1e3a59 100644
--- a/ui/components/scans/forms/schedule-form.tsx
+++ b/ui/components/scans/forms/schedule-form.tsx
@@ -6,10 +6,9 @@ import { useForm } from "react-hook-form";
import * as z from "zod";
import { updateProvider } from "@/actions/providers";
-import { SaveIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
-import { CustomButton, CustomInput } from "@/components/ui/custom";
-import { Form } from "@/components/ui/form";
+import { CustomInput } from "@/components/ui/custom";
+import { Form, FormButtons } from "@/components/ui/form";
import { scheduleScanFormSchema } from "@/types";
export const ScheduleForm = ({
@@ -33,8 +32,6 @@ export const ScheduleForm = ({
const { toast } = useToast();
- const isLoading = form.formState.isSubmitting;
-
const onSubmitClient = async (values: z.infer) => {
const formData = new FormData();
@@ -76,37 +73,13 @@ export const ScheduleForm = ({
labelPlacement="inside"
variant="bordered"
isRequired={false}
- isInvalid={!!form.formState.errors.scheduleDate}
/>
-
- setIsOpen(false)}
- isDisabled={isLoading}
- >
- Cancel
-
-
- }
- isDisabled={true}
- >
- {isLoading ? <>Loading> : Schedule }
-
-
+
);
diff --git a/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx b/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx
index 7d24aa8a72..7fb31d7e2a 100644
--- a/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx
+++ b/ui/components/scans/launch-workflow/launch-scan-workflow-form.tsx
@@ -7,7 +7,8 @@ import * as z from "zod";
import { scanOnDemand } from "@/actions/scans";
import { RocketIcon } from "@/components/icons";
-import { CustomButton, CustomInput } from "@/components/ui/custom";
+import { Button } from "@/components/shadcn";
+import { CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { toast } from "@/components/ui/toast";
import { onDemandScanFormSchema } from "@/types";
@@ -121,7 +122,6 @@ export const LaunchScanWorkflow = ({
size="sm"
variant="bordered"
isRequired={false}
- isInvalid={!!form.formState.errors.scanName}
/>
- }
+ size="default"
+ disabled={isLoading}
+ className="gap-2"
>
- {isLoading ? <>Loading> : Start now }
-
- form.reset()}
- className="w-fit border-gray-200 bg-transparent"
- ariaLabel="Clear form"
- variant="bordered"
- size="sm"
- radius="sm"
+ {!isLoading && }
+ {isLoading ? "Loading..." : "Start now"}
+
+ form.reset()}
+ variant="outline"
+ size="default"
>
Cancel
-
+
>
)}
- {/*
-
-
- {form.watch("providerId") && (
-
-
-
- )}
-
-
*/}
);
diff --git a/ui/components/scans/launch-workflow/select-scan-provider.tsx b/ui/components/scans/launch-workflow/select-scan-provider.tsx
index 372a2f8e2e..3fc7119edf 100644
--- a/ui/components/scans/launch-workflow/select-scan-provider.tsx
+++ b/ui/components/scans/launch-workflow/select-scan-provider.tsx
@@ -1,8 +1,14 @@
"use client";
-import { Select, SelectItem } from "@heroui/select";
import { Control, FieldPath, FieldValues } from "react-hook-form";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/shadcn";
import { EntityInfoShort } from "@/components/ui/entities";
import { FormControl, FormField, FormMessage } from "@/components/ui/form";
@@ -33,77 +39,62 @@ export const SelectScanProvider = <
(
- <>
-
- {
- const selectedValue = Array.from(keys)[0]?.toString();
- field.onChange(selectedValue);
- }}
- renderValue={() => {
- const selectedItem = providers.find(
- (item) => item.providerId === field.value,
- );
- return selectedItem ? (
-
-
-
- ) : (
- "Choose a cloud provider"
- );
- }}
- >
- {providers.map((item) => (
-
-
-
-
-
- ))}
-
-
-
- >
- )}
+ render={({ field }) => {
+ const selectedItem = providers.find(
+ (item) => item.providerId === field.value,
+ );
+
+ return (
+
+
+ Select a cloud provider to launch a scan
+
+
+
+
+
+ {selectedItem ? (
+
+ ) : (
+ "Choose a cloud provider"
+ )}
+
+
+
+ {providers.map((item) => (
+
+
+
+ ))}
+
+
+
+
+
+ );
+ }}
/>
);
};
diff --git a/ui/components/scans/no-providers-added.tsx b/ui/components/scans/no-providers-added.tsx
index 540eec1473..7e9340a6ad 100644
--- a/ui/components/scans/no-providers-added.tsx
+++ b/ui/components/scans/no-providers-added.tsx
@@ -1,43 +1,39 @@
"use client";
-import { Card, CardBody } from "@heroui/card";
-import React from "react";
+import Link from "next/link";
+
+import { Button, Card, CardContent } from "@/components/shadcn";
import { InfoIcon } from "../icons/Icons";
-import { CustomButton } from "../ui/custom";
export const NoProvidersAdded = () => {
return (
-
-
-
-
-
-
- No Cloud Providers Configured
-
-
-
-
- No cloud providers have been configured. Start by setting up a
- cloud provider.
-
-
+
+
+
+
+
+ No Cloud Providers Configured
+
+
+
+
+ No cloud providers have been configured. Start by setting up a
+ cloud provider.
+
+
-
- Get Started
-
-
-
-
+
+ Get Started
+
+
+
);
};
diff --git a/ui/components/scans/no-providers-connected.tsx b/ui/components/scans/no-providers-connected.tsx
index 2ef497f873..2aa4734af1 100644
--- a/ui/components/scans/no-providers-connected.tsx
+++ b/ui/components/scans/no-providers-connected.tsx
@@ -1,14 +1,15 @@
"use client";
-import React from "react";
+import Link from "next/link";
+
+import { Button, Card, CardContent } from "@/components/shadcn";
import { InfoIcon } from "../icons/Icons";
-import { CustomButton } from "../ui/custom";
export const NoProvidersConnected = () => {
return (
-
-
+
+
@@ -26,18 +27,15 @@ export const NoProvidersConnected = () => {
-
- Review Cloud Providers
-
+ Review Cloud Providers
+
-
-
+
+
);
};
diff --git a/ui/components/scans/table/scans/data-table-row-actions.tsx b/ui/components/scans/table/scans/data-table-row-actions.tsx
index ebd2d0f93c..8e284df763 100644
--- a/ui/components/scans/table/scans/data-table-row-actions.tsx
+++ b/ui/components/scans/table/scans/data-table-row-actions.tsx
@@ -1,6 +1,5 @@
"use client";
-import { Button } from "@heroui/button";
import {
Dropdown,
DropdownItem,
@@ -17,6 +16,7 @@ import { DownloadIcon } from "lucide-react";
import { useState } from "react";
import { VerticalDotsIcon } from "@/components/icons";
+import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
import { CustomAlertModal } from "@/components/ui/custom";
import { downloadScanZip } from "@/lib/helper";
@@ -53,12 +53,12 @@ export function DataTableRowActions
({
-
-
+
+
{
return (
-
+
{/* Table headers */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{/* Table body */}
@@ -35,29 +22,15 @@ export const SkeletonTableScans = () => {
{[...Array(3)].map((_, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
))}
diff --git a/ui/components/shadcn/button/button.tsx b/ui/components/shadcn/button/button.tsx
index f81fac5cef..8eab197aac 100644
--- a/ui/components/shadcn/button/button.tsx
+++ b/ui/components/shadcn/button/button.tsx
@@ -5,20 +5,29 @@ import { ComponentProps } from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:bg-button-disabled disabled:text-text-neutral-tertiary outline-none focus-visible:ring-2 focus-visible:ring-offset-2 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0",
{
variants: {
variant: {
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive:
- "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
- outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ default:
+ "bg-button-primary text-black hover:bg-button-primary-hover active:bg-button-primary-press focus-visible:ring-button-primary/50",
secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ "bg-button-secondary text-white hover:bg-button-secondary/90 active:bg-button-secondary-press focus-visible:ring-button-secondary/50 dark:text-black",
+ tertiary:
+ "bg-button-tertiary text-white hover:bg-button-tertiary-hover active:bg-button-tertiary-active focus-visible:ring-button-tertiary/50",
+ destructive:
+ "bg-bg-fail text-white hover:bg-bg-fail/90 active:bg-bg-fail/80 focus-visible:ring-bg-fail/50",
+ outline:
+ "border border-border-neutral-secondary bg-bg-neutral-secondary hover:bg-bg-neutral-tertiary active:bg-border-neutral-tertiary text-text-neutral-primary focus-visible:ring-border-neutral-tertiary/50",
ghost:
- "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
- link: "text-primary underline-offset-4 hover:underline",
+ "text-text-neutral-primary hover:bg-bg-neutral-tertiary active:bg-border-neutral-secondary focus-visible:ring-border-neutral-secondary/50",
+ link: "text-button-tertiary underline-offset-4 hover:text-button-tertiary-hover",
+ // Menu variant like secondary but more padding and the back is almost transparent
+ menu: "backdrop-blur-xl bg-white/60 dark:bg-white/5 border border-white/80 dark:border-white/10 text-text-neutral-primary dark:text-white shadow-lg hover:bg-white/70 dark:hover:bg-white/10 hover:border-white/90 dark:hover:border-white/30 active:bg-white/80 dark:active:bg-white/15 active:scale-[0.98] focus-visible:ring-button-primary/50 transition-all duration-200",
+ "menu-active":
+ "backdrop-blur-xl bg-white/50 dark:bg-white/5 border border-black/[0.08] dark:border-white/10 text-text-neutral-primary dark:text-white shadow-sm hover:bg-white/60 dark:hover:bg-white/10 hover:border-black/[0.12] dark:hover:border-white/30 active:bg-white/70 dark:active:bg-white/15 active:scale-[0.98] focus-visible:ring-button-primary/50 transition-all duration-200",
+ "menu-inactive":
+ "text-text-neutral-primary border border-transparent hover:backdrop-blur-xl hover:bg-white/40 dark:hover:bg-white/5 hover:border-black/[0.08] dark:hover:border-white/10 hover:shadow-sm active:bg-white/50 dark:active:bg-white/15 active:scale-[0.98] focus-visible:ring-border-neutral-secondary/50 transition-all duration-200",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
diff --git a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx
index be3080143d..36ee2c1f8e 100644
--- a/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx
+++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx
@@ -11,7 +11,7 @@ export interface StatItem {
}
const variantColors = {
- default: "var(--bg-neutral-tertiary)",
+ default: "var(--text-neutral-tertiary)",
fail: "var(--bg-fail-primary)",
pass: "var(--bg-pass-primary)",
warning: "var(--bg-warning-primary)",
diff --git a/ui/components/shadcn/combobox/combobox.tsx b/ui/components/shadcn/combobox/combobox.tsx
new file mode 100644
index 0000000000..115c830d31
--- /dev/null
+++ b/ui/components/shadcn/combobox/combobox.tsx
@@ -0,0 +1,208 @@
+"use client";
+
+import { cva, type VariantProps } from "class-variance-authority";
+import { Check, ChevronsUpDown } from "lucide-react";
+import { useState } from "react";
+
+import { Button } from "@/components/shadcn/button/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/shadcn/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/shadcn/popover";
+import { cn } from "@/lib/utils";
+
+const comboboxTriggerVariants = cva("", {
+ variants: {
+ variant: {
+ default:
+ "w-full justify-between rounded-xl border border-border-neutral-secondary bg-bg-neutral-secondary hover:bg-bg-neutral-tertiary",
+ ghost:
+ "border-none bg-transparent shadow-none hover:bg-accent hover:text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+});
+
+const comboboxContentVariants = cva("p-0", {
+ variants: {
+ variant: {
+ default:
+ "w-[calc(100vw-2rem)] max-w-md rounded-xl border border-border-neutral-secondary bg-bg-neutral-secondary shadow-md sm:w-full",
+ ghost:
+ "w-[calc(100vw-2rem)] max-w-md rounded-lg border border-slate-400 bg-white sm:w-full dark:border-[#262626] dark:bg-[#171717]",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+});
+
+export interface ComboboxOption {
+ value: string;
+ label: string;
+}
+
+export interface ComboboxGroup {
+ heading: string;
+ options: ComboboxOption[];
+}
+
+export interface ComboboxProps
+ extends VariantProps {
+ value?: string;
+ onValueChange?: (value: string) => void;
+ options?: ComboboxOption[];
+ groups?: ComboboxGroup[];
+ placeholder?: string;
+ searchPlaceholder?: string;
+ emptyMessage?: string;
+ className?: string;
+ triggerClassName?: string;
+ contentClassName?: string;
+ disabled?: boolean;
+ showSelectedFirst?: boolean;
+}
+
+export function Combobox({
+ value,
+ onValueChange,
+ options = [],
+ groups = [],
+ placeholder = "Select option...",
+ searchPlaceholder = "Search...",
+ emptyMessage = "No option found.",
+ className,
+ triggerClassName,
+ contentClassName,
+ variant = "default",
+ disabled = false,
+ showSelectedFirst = true,
+}: ComboboxProps) {
+ const [open, setOpen] = useState(false);
+
+ const selectedOption =
+ options.find((option) => option.value === value) ||
+ groups
+ .flatMap((group) => group.options)
+ .find((option) => option.value === value);
+
+ const handleSelect = (selectedValue: string) => {
+ onValueChange?.(selectedValue === value ? "" : selectedValue);
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+ {selectedOption ? selectedOption.label : placeholder}
+
+
+
+
+
+
+
+
+ {emptyMessage}
+
+ {/* Show selected option first if enabled */}
+ {showSelectedFirst && selectedOption && (
+
+
+
+ {selectedOption.label}
+
+
+ )}
+
+ {/* Render grouped options */}
+ {groups.length > 0 &&
+ groups.map((group) => {
+ const availableOptions = showSelectedFirst
+ ? group.options.filter((option) => option.value !== value)
+ : group.options;
+
+ if (availableOptions.length === 0) return null;
+
+ return (
+
+ {availableOptions.map((option) => (
+
+
+ {option.label}
+
+ ))}
+
+ );
+ })}
+
+ {/* Render flat options if no groups */}
+ {groups.length === 0 && options.length > 0 && (
+
+ {options
+ .filter(
+ (option) => !showSelectedFirst || option.value !== value,
+ )
+ .map((option) => (
+
+
+ {option.label}
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/ui/components/shadcn/combobox/index.ts b/ui/components/shadcn/combobox/index.ts
new file mode 100644
index 0000000000..d93f84cda6
--- /dev/null
+++ b/ui/components/shadcn/combobox/index.ts
@@ -0,0 +1,2 @@
+export type { ComboboxGroup, ComboboxOption, ComboboxProps } from "./combobox";
+export { Combobox } from "./combobox";
diff --git a/ui/components/shadcn/command.tsx b/ui/components/shadcn/command.tsx
new file mode 100644
index 0000000000..d848db5c27
--- /dev/null
+++ b/ui/components/shadcn/command.tsx
@@ -0,0 +1,188 @@
+"use client";
+
+import { Command as CommandPrimitive } from "cmdk";
+import { SearchIcon } from "lucide-react";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/shadcn/dialog";
+import { cn } from "@/lib/utils";
+
+function Command({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandDialog({
+ title = "Command Palette",
+ description = "Search for a command to run...",
+ children,
+ className,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ title?: string;
+ description?: string;
+ className?: string;
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+ {title}
+ {description}
+
+
+
+ {children}
+
+
+
+ );
+}
+
+function CommandInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ );
+}
+
+function CommandList({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandEmpty({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function CommandShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ );
+}
+
+export {
+ Command,
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+ CommandShortcut,
+};
diff --git a/ui/components/shadcn/dialog.tsx b/ui/components/shadcn/dialog.tsx
new file mode 100644
index 0000000000..6d459a9d25
--- /dev/null
+++ b/ui/components/shadcn/dialog.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ );
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+};
diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts
index 8991f1e34d..b47617530b 100644
--- a/ui/components/shadcn/index.ts
+++ b/ui/components/shadcn/index.ts
@@ -4,7 +4,9 @@ export * from "./card/card";
export * from "./card/resource-stats-card/resource-stats-card";
export * from "./card/resource-stats-card/resource-stats-card-content";
export * from "./card/resource-stats-card/resource-stats-card-header";
+export * from "./combobox";
export * from "./dropdown/dropdown";
+export * from "./select/multiselect";
export * from "./select/select";
export * from "./separator/separator";
export * from "./skeleton/skeleton";
diff --git a/ui/components/shadcn/popover.tsx b/ui/components/shadcn/popover.tsx
new file mode 100644
index 0000000000..711877134f
--- /dev/null
+++ b/ui/components/shadcn/popover.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+
+import { cn } from "@/lib/utils";
+
+function Popover({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function PopoverTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function PopoverContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function PopoverAnchor({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
diff --git a/ui/components/shadcn/select/multiselect.tsx b/ui/components/shadcn/select/multiselect.tsx
new file mode 100644
index 0000000000..5ee63f7748
--- /dev/null
+++ b/ui/components/shadcn/select/multiselect.tsx
@@ -0,0 +1,460 @@
+"use client";
+
+import { CheckIcon, ChevronDown, XIcon } from "lucide-react";
+import {
+ type ComponentPropsWithoutRef,
+ createContext,
+ type ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+
+import { Badge } from "@/components/shadcn/badge/badge";
+import { Button } from "@/components/shadcn/button/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "@/components/shadcn/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/shadcn/popover";
+import { cn } from "@/lib/utils";
+
+type MultiSelectContextType = {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ selectedValues: Set;
+ toggleValue: (value: string) => void;
+ items: Map;
+ onItemAdded: (value: string, label: ReactNode) => void;
+ onValuesChange?: (values: string[]) => void;
+};
+const MultiSelectContext = createContext(null);
+
+export function MultiSelect({
+ children,
+ values,
+ defaultValues,
+ onValuesChange,
+}: {
+ children: ReactNode;
+ values?: string[];
+ defaultValues?: string[];
+ onValuesChange?: (values: string[]) => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const [internalValues, setInternalValues] = useState(
+ new Set(values ?? defaultValues),
+ );
+ const selectedValues = values ? new Set(values) : internalValues;
+ const [items, setItems] = useState>(new Map());
+
+ function toggleValue(value: string) {
+ const getNewSet = (prev: Set) => {
+ const newSet = new Set(prev);
+ if (newSet.has(value)) {
+ newSet.delete(value);
+ } else {
+ newSet.add(value);
+ }
+ return newSet;
+ };
+ setInternalValues(getNewSet);
+ onValuesChange?.(Array.from(getNewSet(selectedValues)));
+ }
+
+ const onItemAdded = useCallback((value: string, label: ReactNode) => {
+ setItems((prev) => {
+ if (prev.get(value) === label) return prev;
+ return new Map(prev).set(value, label);
+ });
+ }, []);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export function MultiSelectTrigger({
+ className,
+ children,
+ size = "default",
+ ...props
+}: {
+ className?: string;
+ children?: ReactNode;
+ size?: "sm" | "default";
+} & ComponentPropsWithoutRef) {
+ const { open } = useMultiSelectContext();
+
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export function MultiSelectValue({
+ placeholder,
+ clickToRemove = true,
+ className,
+ overflowBehavior = "wrap-when-open",
+ ...props
+}: {
+ placeholder?: string;
+ clickToRemove?: boolean;
+ overflowBehavior?: "wrap" | "wrap-when-open" | "cutoff";
+} & Omit, "children">) {
+ const { selectedValues, toggleValue, items, open } = useMultiSelectContext();
+ const [overflowAmount, setOverflowAmount] = useState(0);
+ const valueRef = useRef(null);
+ const overflowRef = useRef(null);
+
+ const shouldWrap =
+ overflowBehavior === "wrap" ||
+ (overflowBehavior === "wrap-when-open" && open);
+
+ const checkOverflow = useCallback(() => {
+ if (valueRef.current === null) return;
+
+ const containerElement = valueRef.current;
+ const overflowElement = overflowRef.current;
+ const items = containerElement.querySelectorAll(
+ "[data-selected-item]",
+ );
+
+ if (overflowElement !== null) overflowElement.style.display = "none";
+ items.forEach((child) => child.style.removeProperty("display"));
+ let amount = 0;
+ for (let i = items.length - 1; i >= 0; i--) {
+ const child = items[i]!;
+ if (containerElement.scrollWidth <= containerElement.clientWidth) {
+ break;
+ }
+ amount = items.length - i;
+ child.style.display = "none";
+ overflowElement?.style.removeProperty("display");
+ }
+ setOverflowAmount(amount);
+ }, []);
+
+ const handleResize = useCallback(
+ (node: HTMLDivElement) => {
+ valueRef.current = node;
+
+ const mutationObserver = new MutationObserver(checkOverflow);
+ const observer = new ResizeObserver(debounce(checkOverflow, 100));
+
+ mutationObserver.observe(node, {
+ childList: true,
+ attributes: true,
+ attributeFilter: ["class", "style"],
+ });
+ observer.observe(node);
+
+ return () => {
+ observer.disconnect();
+ mutationObserver.disconnect();
+ valueRef.current = null;
+ };
+ },
+ [checkOverflow],
+ );
+
+ return (
+
+ {placeholder && (
+
+ {placeholder}
+
+ )}
+ {Array.from(selectedValues)
+ .filter((value) => items.has(value))
+ .map((value) => (
+ {
+ e.stopPropagation();
+ toggleValue(value);
+ }
+ : undefined
+ }
+ >
+ {items.get(value)}
+ {clickToRemove && (
+
+ )}
+
+ ))}
+ 0 && !shouldWrap ? "block" : "none",
+ }}
+ variant="outline"
+ ref={overflowRef}
+ className="text-bg-button-secondary border-slate-300 bg-slate-100 px-2 py-1 text-xs font-medium dark:border-slate-600 dark:bg-slate-800"
+ >
+ +{overflowAmount}
+
+
+ );
+}
+
+export function MultiSelectContent({
+ search = true,
+ children,
+ width = "default",
+ ...props
+}: {
+ search?: boolean | { placeholder?: string; emptyMessage?: string };
+ children: ReactNode;
+ width?: "default" | "wide";
+} & Omit, "children">) {
+ const canSearch = typeof search === "object" ? true : search;
+
+ const widthClasses =
+ width === "wide" ? "w-auto min-w-[400px] max-w-[600px]" : "w-auto";
+
+ return (
+ <>
+
+
+ {children}
+
+
+
+
+ {canSearch ? (
+
+ ) : (
+
+ )}
+
+
+ {canSearch && (
+
+ {typeof search === "object" ? search.emptyMessage : undefined}
+
+ )}
+ {children}
+
+
+
+
+ >
+ );
+}
+
+export function MultiSelectItem({
+ value,
+ children,
+ badgeLabel,
+ onSelect,
+ className,
+ ...props
+}: {
+ badgeLabel?: ReactNode;
+ value: string;
+} & Omit, "value">) {
+ const { toggleValue, selectedValues, onItemAdded } = useMultiSelectContext();
+ const isSelected = selectedValues.has(value);
+
+ useEffect(() => {
+ onItemAdded(value, badgeLabel ?? children);
+ }, [value, children, onItemAdded, badgeLabel]);
+
+ return (
+ {
+ toggleValue(value);
+ onSelect?.(value);
+ }}
+ >
+ {children}
+
+
+ );
+}
+
+export function MultiSelectGroup(
+ props: ComponentPropsWithoutRef,
+) {
+ return ;
+}
+
+export function MultiSelectSeparator({
+ className,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+ );
+}
+
+export function MultiSelectSelectAll({
+ className,
+ children = "Select All",
+ allValues = [],
+ ...props
+}: Omit, "children"> & {
+ children?: ReactNode;
+ allValues?: string[];
+}) {
+ const { selectedValues, onValuesChange } = useMultiSelectContext();
+
+ if (!onValuesChange) {
+ return null;
+ }
+
+ const selectedArray = Array.from(selectedValues);
+ const allSelected =
+ allValues.length > 0 && selectedArray.length === allValues.length;
+
+ const handleSelectAll = () => {
+ if (allSelected) {
+ // Deselect all
+ onValuesChange?.([]);
+ } else {
+ // Select all
+ onValuesChange?.(allValues);
+ }
+ };
+
+ return (
+ {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ handleSelectAll();
+ }
+ }}
+ tabIndex={0}
+ {...props}
+ >
+ {children}
+
+
+ );
+}
+
+function useMultiSelectContext() {
+ const context = useContext(MultiSelectContext);
+ if (context === null) {
+ throw new Error(
+ "useMultiSelectContext must be used within a MultiSelectContext",
+ );
+ }
+ return context;
+}
+
+function debounce void>(
+ func: T,
+ wait: number,
+): (...args: Parameters) => void {
+ let timeout: ReturnType | null = null;
+ return function (this: unknown, ...args: Parameters) {
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(() => func.apply(this, args), wait);
+ };
+}
diff --git a/ui/components/shadcn/select/select.tsx b/ui/components/shadcn/select/select.tsx
index eed5f8e679..b56e3f186c 100644
--- a/ui/components/shadcn/select/select.tsx
+++ b/ui/components/shadcn/select/select.tsx
@@ -1,99 +1,33 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
-import { CheckIcon, ChevronDownIcon, ChevronUpIcon, X } from "lucide-react";
-import {
- ComponentProps,
- createContext,
- KeyboardEvent,
- MouseEvent,
- useContext,
- useId,
-} from "react";
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
+import { ComponentProps } from "react";
import { cn } from "@/lib/utils";
-// Context for managing multi-select state
-type SelectContextValue = {
- multiple?: boolean;
- selectedValues?: string[];
- onMultiValueChange?: (values: string[]) => void;
- ariaLabel?: string;
- liveRegionId?: string;
-};
-
-const SelectContext = createContext({});
-
function Select({
allowDeselect = false,
- multiple = false,
- value,
- onValueChange,
- selectedValues = [],
- onMultiValueChange,
- ariaLabel,
...props
-}: Omit, "onValueChange"> & {
+}: ComponentProps & {
allowDeselect?: boolean;
- multiple?: boolean;
- selectedValues?: string[];
- onValueChange?: (value: string) => void;
- onMultiValueChange?: (values: string[]) => void;
- ariaLabel?: string;
}) {
- const liveRegionId = useId();
-
const handleValueChange = (nextValue: string) => {
- if (multiple && onMultiValueChange) {
- // Multi-select: toggle the value
- const newValues = selectedValues.includes(nextValue)
- ? selectedValues.filter((v) => v !== nextValue)
- : [...selectedValues, nextValue];
- onMultiValueChange(newValues);
- } else if (
- allowDeselect &&
- typeof value === "string" &&
- value === nextValue
- ) {
+ if (allowDeselect && props.value === nextValue) {
// Single-select with deselect
- onValueChange?.("");
+ props.onValueChange?.("");
} else {
// Single-select
- onValueChange?.(nextValue);
+ props.onValueChange?.(nextValue);
}
};
- const contextValue = {
- multiple,
- selectedValues,
- onMultiValueChange,
- ariaLabel,
- liveRegionId,
- };
-
return (
-
-
- {/* Live region for screen reader announcements */}
- {multiple && (
-
- {selectedValues.length > 0
- ? `${selectedValues.length} ${selectedValues.length === 1 ? "item" : "items"} selected`
- : "No items selected"}
-
- )}
-
+
);
}
@@ -104,31 +38,9 @@ function SelectGroup({
}
function SelectValue({
- placeholder,
- children,
...props
}: ComponentProps) {
- const { multiple, selectedValues } = useContext(SelectContext);
-
- // For multi-select, render custom children or placeholder
- if (multiple) {
- return (
-
- {selectedValues && selectedValues.length > 0 ? children : placeholder}
-
- );
- }
-
- // For single-select, use default Radix behavior
- return (
-
- {children}
-
- );
+ return ;
}
function SelectTrigger({
@@ -139,63 +51,23 @@ function SelectTrigger({
}: ComponentProps & {
size?: "sm" | "default";
}) {
- const { multiple, selectedValues, onMultiValueChange, ariaLabel } =
- useContext(SelectContext);
- const hasSelection = multiple && selectedValues && selectedValues.length > 0;
-
- const handleClear = (
- e: MouseEvent | KeyboardEvent,
- ) => {
- e.stopPropagation();
- if (onMultiValueChange) {
- onMultiValueChange([]);
- }
- };
-
- const clearButtonLabel = `Clear ${ariaLabel || "selection"}${hasSelection ? ` (${selectedValues.length} selected)` : ""}`;
-
return (
{children}
-
- {hasSelection && (
- {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- e.stopPropagation();
- handleClear(e);
- }
- }}
- className="pointer-events-auto cursor-pointer rounded-sm p-0.5 opacity-70 transition-opacity hover:opacity-100 focus:opacity-100 focus:ring-2 focus:ring-slate-600 focus:ring-offset-2 focus:outline-none dark:focus:ring-slate-400"
- aria-label={clearButtonLabel}
- >
-
-
- )}
-
-
-
-
+
+
+
);
}
@@ -204,7 +76,7 @@ function SelectContent({
className,
children,
position = "popper",
- align = "center",
+ align = "start",
...props
}: ComponentProps) {
return (
@@ -212,7 +84,7 @@ function SelectContent({
);
@@ -253,22 +125,13 @@ function SelectLabel({
function SelectItem({
className,
children,
- value,
...props
}: ComponentProps) {
- const { multiple, selectedValues } = useContext(SelectContext);
- const isSelected = multiple && selectedValues?.includes(value);
-
return (
{children}
-
- {multiple ? (
- // Multi-select: show check when selected
- isSelected && (
-
- )
- ) : (
- // Single-select: use radix indicator
-
-
-
- )}
-
+
+
+
);
}
@@ -322,7 +172,7 @@ function SelectScrollUpButton({
)}
{...props}
>
-
+
);
}
@@ -340,7 +190,7 @@ function SelectScrollDownButton({
)}
{...props}
>
-
+
);
}
diff --git a/ui/components/shadcn/tooltip.tsx b/ui/components/shadcn/tooltip.tsx
new file mode 100644
index 0000000000..f7c520c08c
--- /dev/null
+++ b/ui/components/shadcn/tooltip.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cn } from "@/lib/utils";
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/ui/components/ui/accordion/Accordion.tsx b/ui/components/ui/accordion/Accordion.tsx
index 54527ce08a..efb390bda7 100644
--- a/ui/components/ui/accordion/Accordion.tsx
+++ b/ui/components/ui/accordion/Accordion.tsx
@@ -40,9 +40,14 @@ const AccordionContent = ({
selectedKeys?: string[];
onSelectionChange?: (keys: string[]) => void;
}) => {
+ // Normalize possible array content to automatically assign stable keys
+ const normalizedContent = Array.isArray(content)
+ ? React.Children.toArray(content)
+ : content;
+
return (
- {content}
+ {normalizedContent}
{items && items.length > 0 && (
(
return {
card: "border-danger-300",
iconWrapper: "bg-danger-50 border-danger-100",
- icon: "text-danger",
+ icon: "text-text-error",
};
default:
diff --git a/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx b/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx
index 4b778e84fd..480e569887 100644
--- a/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx
+++ b/ui/components/ui/breadcrumbs/breadcrumb-navigation.tsx
@@ -107,14 +107,19 @@ export function BreadcrumbNavigation({
const renderTitleWithIcon = (titleText: string, isLink: boolean = false) => (
<>
{typeof icon === "string" ? (
-
+
) : icon ? (
{icon}
) : null}
{titleText}
@@ -150,7 +155,7 @@ export function BreadcrumbNavigation({
>
{breadcrumb.icon && typeof breadcrumb.icon === "string" ? (
) : null}
-
+
{breadcrumb.name}
) : breadcrumb.isClickable && breadcrumb.onClick ? (
{breadcrumb.icon && typeof breadcrumb.icon === "string" ? (
) : null}
- {breadcrumb.name}
+
+ {breadcrumb.name}
+
) : (
@@ -197,7 +204,7 @@ export function BreadcrumbNavigation({
{breadcrumb.icon}
) : null}
-
+
{breadcrumb.name}
diff --git a/ui/components/ui/button/button.tsx b/ui/components/ui/button/button.tsx
index 05c76e5127..3217c0d897 100644
--- a/ui/components/ui/button/button.tsx
+++ b/ui/components/ui/button/button.tsx
@@ -5,7 +5,7 @@ import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-[14px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
@@ -16,9 +16,9 @@ const buttonVariants = cva(
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
- "bg-default-100 text-default-900 shadow-sm dark:bg-prowler-blue-800 font-bold",
+ "border-2 border-slate-950 text-neutral-primary dark:border-white dark:text-neutral-primary font-bold px-[14px]",
ghost:
- "hover:bg-accent hover:text-accent-foreground text-default-600 hover:font-bold hover:bg-default-100 dark:hover:bg-prowler-blue-800",
+ "border-2 border-transparent text-neutral-secondary dark:text-neutral-secondary hover:border-slate-950 dark:hover:border-white hover:font-bold px-[14px]",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
diff --git a/ui/components/ui/chart/Chart.tsx b/ui/components/ui/chart/Chart.tsx
index 014fbc009d..2a590794fd 100644
--- a/ui/components/ui/chart/Chart.tsx
+++ b/ui/components/ui/chart/Chart.tsx
@@ -3,14 +3,8 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
-// import {
-// NameType,
-// Payload,
-// ValueType,
-// } from "recharts/types/component/DefaultTooltipContent";
import { cn } from "@/lib/utils";
-// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
diff --git a/ui/components/ui/chart/horizontal-split-chart.tsx b/ui/components/ui/chart/horizontal-split-chart.tsx
deleted file mode 100644
index 70308d60c6..0000000000
--- a/ui/components/ui/chart/horizontal-split-chart.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-"use client";
-
-import { Tooltip } from "@heroui/tooltip";
-import * as React from "react";
-import { useEffect, useState } from "react";
-
-import { cn } from "@/lib/utils";
-
-interface HorizontalSplitBarProps {
- /**
- * First value (left)
- */
- valueA: number;
- /**
- * Second value (right)
- */
- valueB: number;
- /**
- * Additional CSS classes for the main container
- */
- className?: string;
- /**
- * Color for value A (Tailwind classes)
- * @default "bg-system-success"
- */
- colorA?: string;
- /**
- * Color for value B (Tailwind classes)
- * @default "bg-system-error"
- */
- colorB?: string;
- /**
- * Value format suffix (like "%", "$", etc.)
- * Will be appended to the values when displayed
- * @example "%"
- */
- valueSuffix?: string;
- /**
- * Bar height
- * @default "h-4"
- */
- barHeight?: string;
- /**
- * Color for the empty state (when both values are 0)
- * @default "bg-gray-300"
- */
- emptyColor?: string;
- /**
- * Text to display when there is no data
- * @default "No data available"
- */
- emptyText?: string;
- /**
- * Minimum width for small values (in pixels)
- * @default 25
- */
- minBarWidth?: number;
- /**
- * Custom tooltip content for value A (optional)
- * If not provided, the formatted value will be used
- */
- tooltipContentA?: string;
- /**
- * Custom tooltip content for value B (optional)
- * If not provided, the formatted value will be used
- */
- tooltipContentB?: string;
- /**
- * Text color for labels
- * @default "text-gray-700"
- */
- labelColor?: string;
- /**
- * Growth ratio multiplier (pixels per value unit)
- * @default 1
- */
- ratio?: number;
- /**
- * Show zero values in labels
- * @default true
- */
- showZero?: boolean;
-}
-
-/**
- * Horizontal split bar chart component that displays two values
- * with bars growing from a central separator.
- *
- * @example
- * ```tsx
- *
- * ```
- */
-export const HorizontalSplitBar = ({
- valueA,
- valueB,
- className,
- colorA = "bg-system-success",
- colorB = "bg-system-error",
- valueSuffix = "",
- barHeight = "h-4",
- emptyColor = "bg-gray-300",
- emptyText = "No data available",
- minBarWidth = 25,
- tooltipContentA,
- tooltipContentB,
- labelColor = "text-gray-700",
- ratio = 1,
- showZero = true,
-}: HorizontalSplitBarProps) => {
- // Reference to the container to measure its width
- const containerRef = React.useRef
(null);
- const [maxContainerWidth, setMaxContainerWidth] = useState(0);
-
- // Effect to measure the container width
- useEffect(() => {
- if (containerRef.current) {
- const updateWidth = () => {
- const containerWidth = containerRef.current?.clientWidth || 0;
- setMaxContainerWidth(containerWidth);
- };
-
- updateWidth();
-
- window.addEventListener("resize", updateWidth);
- return () => window.removeEventListener("resize", updateWidth);
- }
- }, []);
-
- // Ensure values are positive
- const valA = Math.max(0, valueA);
- const valB = Math.max(0, valueB);
-
- const hasNoData = valA === 0 && valB === 0;
- const formattedValueA = `${valA}${valueSuffix}`;
- const formattedValueB = `${valB}${valueSuffix}`;
-
- if (hasNoData) {
- return (
-
- );
- }
-
- const availableWidth = Math.max(0, maxContainerWidth);
- const halfWidth = availableWidth / 2;
- const separatorWidth = 1;
-
- // Apply ratio multiplier to raw widths
- let rawWidthA = valA * ratio;
- let rawWidthB = valB * ratio;
-
- // Determine if we need to scale to fit in available space
- const maxSideWidth = halfWidth - separatorWidth / 2;
- const needsScaling = rawWidthA > maxSideWidth || rawWidthB > maxSideWidth;
-
- if (needsScaling) {
- // Calculate scale factor based on the largest value
- const maxRawWidth = Math.max(rawWidthA, rawWidthB);
- const scaleFactor = maxSideWidth / maxRawWidth;
-
- // Apply the scale factor to both sides
- rawWidthA = rawWidthA * scaleFactor;
- rawWidthB = rawWidthB * scaleFactor;
- }
-
- // Apply minimum width if needed
- const barWidthA = Math.max(rawWidthA, valA > 0 ? minBarWidth : 0);
- const barWidthB = Math.max(rawWidthB, valB > 0 ? minBarWidth : 0);
-
- return (
-
-
-
- {/* Left label */}
-
- {valA > 0 ? formattedValueA : showZero ? "0" : ""}
-
- {/* Left bar */}
- {valA > 0 && (
-
-
-
- )}
-
-
- {/* Central separator */}
-
-
-
- {/* Right bar */}
- {valB > 0 && (
-
-
-
- )}
- {/* Right label */}
-
- {valB > 0 ? formattedValueB : showZero ? "0" : ""}
-
-
-
-
- );
-};
-
-export default HorizontalSplitBar;
diff --git a/ui/components/ui/code-snippet/code-snippet.tsx b/ui/components/ui/code-snippet/code-snippet.tsx
index 4acdfcc522..f63ab792c9 100644
--- a/ui/components/ui/code-snippet/code-snippet.tsx
+++ b/ui/components/ui/code-snippet/code-snippet.tsx
@@ -2,7 +2,7 @@ import { Snippet } from "@heroui/snippet";
export const CodeSnippet = ({ value }: { value: string }) => (
- {children}
+ {children}
>
);
}
diff --git a/ui/components/ui/custom/custom-alert-modal.tsx b/ui/components/ui/custom/custom-alert-modal.tsx
index b7a284bea9..940b773502 100644
--- a/ui/components/ui/custom/custom-alert-modal.tsx
+++ b/ui/components/ui/custom/custom-alert-modal.tsx
@@ -24,7 +24,7 @@ export const CustomAlertModal: React.FC = ({
onOpenChange={onOpenChange}
size={size}
classNames={{
- base: "dark:bg-prowler-blue-800",
+ base: "border border-border-neutral-secondary bg-bg-neutral-secondary",
closeButton: "rounded-md",
}}
backdrop="blur"
diff --git a/ui/components/ui/custom/custom-banner.tsx b/ui/components/ui/custom/custom-banner.tsx
index 0c92c6ac42..0969721368 100644
--- a/ui/components/ui/custom/custom-banner.tsx
+++ b/ui/components/ui/custom/custom-banner.tsx
@@ -1,8 +1,9 @@
"use client";
import { InfoIcon } from "lucide-react";
+import Link from "next/link";
-import { CustomButton } from ".";
+import { Button, Card, CardContent } from "@/components/shadcn";
interface CustomBannerProps {
title: string;
@@ -18,30 +19,31 @@ export const CustomBanner = ({
buttonLink = "/",
}: CustomBannerProps) => {
return (
-
-
-
-
-
-
- {title}
-
+
+
+
+
+
+
+
+ {title}
+
+
+
+ {message}
+
+
+
+
+ {buttonLabel}
+
-
{message}
-
-
- {buttonLabel}
-
-
-
-
+
+
);
};
diff --git a/ui/components/ui/custom/custom-box.tsx b/ui/components/ui/custom/custom-box.tsx
deleted file mode 100644
index 86c0c36dd4..0000000000
--- a/ui/components/ui/custom/custom-box.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { CardProps as NextUICardProps } from "@heroui/card";
-import { Card, CardBody, CardHeader } from "@heroui/card";
-import { Divider } from "@heroui/divider";
-import React from "react";
-interface CustomBoxProps {
- children: React.ReactNode;
- preTitle?: string;
- subTitle?: string;
- title?: string;
-}
-
-export const CustomBox = ({
- children,
- preTitle,
- subTitle,
- title,
- ...props
-}: CustomBoxProps & NextUICardProps & React.HTMLAttributes
) => {
- return (
-
- {(preTitle || subTitle || title) && (
- <>
-
- {preTitle && (
- {preTitle}
- )}
- {subTitle && {subTitle} }
- {title && {title} }
-
-
- >
- )}
- {children}
-
- );
-};
diff --git a/ui/components/ui/custom/custom-button.tsx b/ui/components/ui/custom/custom-button.tsx
deleted file mode 100644
index 8f71aabf24..0000000000
--- a/ui/components/ui/custom/custom-button.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { Button } from "@heroui/button";
-import { CircularProgress } from "@heroui/progress";
-import type { PressEvent } from "@react-types/shared";
-import clsx from "clsx";
-import Link from "next/link";
-import React from "react";
-
-import { NextUIColors, NextUIVariants } from "@/types";
-
-export const buttonClasses = {
- base: "px-4 inline-flex items-center justify-center relative z-0 text-center whitespace-nowrap",
- primary:
- "bg-default-100 hover:bg-default-200 text-default-800 dark:bg-prowler-blue-800",
- secondary: "bg-prowler-grey-light dark:bg-prowler-grey-medium text-white",
- action: "bg-prowler-theme-green font-bold text-prowler-theme-midnight",
- dashed:
- "border border-default border-dashed bg-transparent justify-center whitespace-nowrap font-medium shadow-sm hover:border-solid hover:bg-default-100 active:bg-default-200 active:border-solid",
- transparent: "border-0 border-transparent bg-transparent",
- disabled: "pointer-events-none opacity-40",
- hover: "hover:shadow-md",
-};
-
-interface CustomButtonProps {
- type?: "button" | "submit" | "reset";
- target?: "_self" | "_blank";
- ariaLabel: string;
- ariaDisabled?: boolean;
- className?: string;
- variant?:
- | "solid"
- | "faded"
- | "bordered"
- | "light"
- | "flat"
- | "ghost"
- | "dashed"
- | "shadow";
- color?:
- | "primary"
- | "secondary"
- | "action"
- | "success"
- | "warning"
- | "danger"
- | "transparent";
- onPress?: (e: PressEvent) => void;
- children?: React.ReactNode;
- startContent?: React.ReactNode;
- endContent?: React.ReactNode;
- size?: "sm" | "md" | "lg";
- radius?: "none" | "sm" | "md" | "lg" | "full";
- dashed?: boolean;
- isDisabled?: boolean;
- isLoading?: boolean;
- isIconOnly?: boolean;
- ref?: React.RefObject;
- asLink?: string;
-}
-
-export const CustomButton = React.forwardRef<
- HTMLButtonElement,
- CustomButtonProps
->(
- (
- {
- type = "button",
- target = "_self",
- ariaLabel,
- ariaDisabled,
- className,
- variant = "solid",
- color = "primary",
- onPress,
- children,
- startContent,
- endContent,
- size = "md",
- radius = "sm",
- isDisabled = false,
- isLoading = false,
- isIconOnly,
- asLink,
- ...props
- },
- ref,
- ) => (
-
- }
- ref={ref}
- isDisabled={isDisabled}
- isLoading={isLoading}
- isIconOnly={isIconOnly}
- {...props}
- >
- {children}
-
- ),
-);
-
-CustomButton.displayName = "CustomButton";
diff --git a/ui/components/ui/custom/custom-dropdown-filter.tsx b/ui/components/ui/custom/custom-dropdown-filter.tsx
deleted file mode 100644
index 9fa24b5ec5..0000000000
--- a/ui/components/ui/custom/custom-dropdown-filter.tsx
+++ /dev/null
@@ -1,349 +0,0 @@
-"use client";
-
-import { Button } from "@heroui/button";
-import { Checkbox, CheckboxGroup } from "@heroui/checkbox";
-import { Divider } from "@heroui/divider";
-import { Popover, PopoverContent, PopoverTrigger } from "@heroui/popover";
-import { ScrollShadow } from "@heroui/scroll-shadow";
-import { ChevronDown, X } from "lucide-react";
-import { useSearchParams } from "next/navigation";
-import React, {
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-
-import { ComplianceScanInfo } from "@/components/compliance/compliance-header/compliance-scan-info";
-import { EntityInfoShort } from "@/components/ui/entities";
-import { isConnectionStatus, isScanEntity } from "@/lib/helper-filters";
-import {
- CustomDropdownFilterProps,
- FilterEntity,
- ProviderEntity,
- ScanEntity,
-} from "@/types";
-
-export const CustomDropdownFilter = ({
- filter,
- onFilterChange,
-}: CustomDropdownFilterProps) => {
- const searchParams = useSearchParams();
- const [groupSelected, setGroupSelected] = useState(new Set());
- const [isOpen, setIsOpen] = useState(false);
- const hasUserInteracted = useRef(false);
-
- const filterValues = useMemo(() => filter?.values || [], [filter?.values]);
- const selectedValues = Array.from(groupSelected).filter(
- (value) => value !== "all",
- );
- const isAllSelected =
- selectedValues.length === filterValues.length && filterValues.length > 0;
-
- const activeFilterValue = useMemo(() => {
- const filterParam = searchParams.get(`filter[${filter?.key}]`);
- return filterParam ? filterParam.split(",") : [];
- }, [searchParams, filter?.key]);
-
- // Helper function to handle URL filter values sync
- const syncWithActiveFilters = useCallback(() => {
- const newSelection = new Set(activeFilterValue);
- if (
- newSelection.size === filterValues.length &&
- filter?.showSelectAll !== false
- ) {
- newSelection.add("all");
- }
- setGroupSelected(newSelection);
- }, [activeFilterValue, filterValues, filter?.showSelectAll]);
-
- const resetComponentState = useCallback(() => {
- setGroupSelected(new Set());
- hasUserInteracted.current = false;
- }, []);
-
- const applyDefaultValues = useCallback(() => {
- if (filter?.defaultToSelectAll && filterValues.length > 0) {
- const newSelection = new Set(filterValues);
- if (filter?.showSelectAll !== false) {
- newSelection.add("all");
- }
- setGroupSelected(newSelection);
- } else if (filter?.defaultValues && filter.defaultValues.length > 0) {
- const validDefaultValues = filter.defaultValues.filter((value) =>
- filterValues.includes(value),
- );
- const newSelection = new Set(validDefaultValues);
-
- // Add "all" if all items are selected and showSelectAll is not false
- if (
- validDefaultValues.length === filterValues.length &&
- filter?.showSelectAll !== false
- ) {
- newSelection.add("all");
- }
- setGroupSelected(newSelection);
- } else {
- setGroupSelected(new Set());
- }
- }, [
- filterValues,
- filter?.defaultToSelectAll,
- filter?.defaultValues,
- filter?.showSelectAll,
- ]);
-
- useEffect(() => {
- const hasActiveFilters = activeFilterValue.length > 0;
- const userHasInteracted = hasUserInteracted.current;
-
- if (hasActiveFilters) {
- // URL has filter values - sync component state with URL
- syncWithActiveFilters();
- } else if (userHasInteracted) {
- // URL has no filters but user had interacted - reset component state
- resetComponentState();
- } else {
- // URL has no filters and user hasn't interacted - apply defaults
- applyDefaultValues();
- }
- }, [
- activeFilterValue,
- syncWithActiveFilters,
- resetComponentState,
- applyDefaultValues,
- ]);
-
- const updateSelection = useCallback(
- (newValues: string[]) => {
- // Mark that user has interacted with the filter
- hasUserInteracted.current = true;
-
- const actualValues = newValues.filter((key) => key !== "all");
- const newSelection = new Set(actualValues);
-
- // Auto-add "all" if all items are selected and showSelectAll is not false
- if (
- actualValues.length === filterValues.length &&
- filterValues.length > 0 &&
- filter?.showSelectAll !== false
- ) {
- newSelection.add("all");
- }
-
- setGroupSelected(newSelection);
-
- // Notify parent with actual values (excluding "all")
- onFilterChange?.(filter.key, actualValues);
- },
- [filterValues.length, onFilterChange, filter.key, filter?.showSelectAll],
- );
-
- const onSelectionChange = useCallback(
- (keys: string[]) => {
- const currentSelection = Array.from(groupSelected);
- const newKeys = new Set(keys);
- const oldKeys = new Set(currentSelection);
-
- // Check if "all" was just toggled
- const allWasSelected = oldKeys.has("all");
- const allIsSelected = newKeys.has("all");
-
- if (allIsSelected && !allWasSelected) {
- // "all" was just selected - select all items
- updateSelection(filterValues);
- } else if (!allIsSelected && allWasSelected) {
- // "all" was just deselected - deselect all items
- updateSelection([]);
- } else if (allIsSelected && allWasSelected) {
- // "all" was already selected, but individual items changed
- // Remove "all" and keep only the individual selections
- const individualSelections = keys.filter((key) => key !== "all");
- updateSelection(individualSelections);
- } else {
- // Normal individual selection without "all"
- updateSelection(keys);
- }
- },
- [groupSelected, updateSelection, filterValues],
- );
-
- const handleClearAll = useCallback(
- (e: React.MouseEvent) => {
- e.stopPropagation();
- updateSelection([]);
- },
- [updateSelection],
- );
-
- const getDisplayLabel = useCallback(
- (value: string) => {
- const entity: FilterEntity | undefined = filter.valueLabelMapping?.find(
- (entry) => entry[value],
- )?.[value];
- if (!entity) return value;
-
- if (isConnectionStatus(entity)) {
- return entity.label;
- }
-
- if (isScanEntity(entity as ScanEntity)) {
- return (
- (entity as ScanEntity).attributes?.name ||
- (entity as ScanEntity).providerInfo?.alias ||
- (entity as ScanEntity).providerInfo?.uid ||
- value
- );
- } else {
- return (
- (entity as ProviderEntity).alias ||
- (entity as ProviderEntity).uid ||
- value
- );
- }
- },
- [filter.valueLabelMapping],
- );
-
- return (
-
-
-
-
- }
- size="md"
- variant="flat"
- >
-
-
- {filter?.labelCheckboxGroup}
-
-
- {selectedValues.length > 0 && (
- <>
-
-
- {selectedValues.length <= 2 ? (
-
- {selectedValues.map(getDisplayLabel).join(", ")}
-
- ) : (
-
- {isAllSelected
- ? "All selected"
- : `${selectedValues.length} selected`}
-
- )}
-
{
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- handleClearAll(e as unknown as React.MouseEvent);
- }
- }}
- >
-
-
-
- >
- )}
-
-
-
-
-
-
- {filterValues.length === 0 && (
- No results found
- )}
- {filter?.showSelectAll !== false && filterValues.length > 0 && (
- <>
-
- Select All
-
-
- >
- )}
- {filterValues.length > 0 && (
-
- {filterValues.map((value) => {
- const entity: FilterEntity | undefined =
- filter.valueLabelMapping?.find((entry) => entry[value])?.[
- value
- ];
-
- return (
-
- {entity ? (
- isConnectionStatus(entity) ? (
- getDisplayLabel(value)
- ) : isScanEntity(entity as ScanEntity) ? (
-
- ) : (
-
- )
- ) : (
- getDisplayLabel(value)
- )}
-
- );
- })}
-
- )}
-
-
-
-
-
- );
-};
diff --git a/ui/components/ui/custom/custom-dropdown-selection.tsx b/ui/components/ui/custom/custom-dropdown-selection.tsx
index 4fb2563d65..4f7e3d6bf0 100644
--- a/ui/components/ui/custom/custom-dropdown-selection.tsx
+++ b/ui/components/ui/custom/custom-dropdown-selection.tsx
@@ -1,13 +1,14 @@
"use client";
-import { Button } from "@heroui/button";
-import { Checkbox, CheckboxGroup } from "@heroui/checkbox";
-import { Divider } from "@heroui/divider";
-import { Popover, PopoverContent, PopoverTrigger } from "@heroui/popover";
-import { ScrollShadow } from "@heroui/scroll-shadow";
-import React, { useCallback, useEffect, useMemo, useState } from "react";
+import React, { useCallback } from "react";
-import { PlusCircleIcon } from "@/components/icons";
+import {
+ MultiSelect,
+ MultiSelectContent,
+ MultiSelectItem,
+ MultiSelectTrigger,
+ MultiSelectValue,
+} from "@/components/shadcn/select/multiselect";
interface CustomDropdownSelectionProps {
label: string;
@@ -17,130 +18,36 @@ interface CustomDropdownSelectionProps {
selectedKeys?: string[];
}
-const selectedTagClass =
- "inline-flex items-center border py-1 text-xs transition-colors border-transparent bg-default-500 text-secondary-foreground hover:bg-default-500/80 rounded-md px-2 font-normal";
-
export const CustomDropdownSelection: React.FC<
CustomDropdownSelectionProps
> = ({ label, name, values, onChange, selectedKeys = [] }) => {
- const [selectedValues, setSelectedValues] = useState>(
- new Set(selectedKeys),
- );
-
- const allValues = useMemo(() => values.map((item) => item.id), [values]);
-
- // Update internal state when selectedKeys changes
- useEffect(() => {
- const newSelection = new Set(selectedKeys);
- if (selectedKeys.length === allValues.length) {
- newSelection.add("all");
- }
- setSelectedValues(newSelection);
- }, [selectedKeys, allValues]);
-
- const onSelectionChange = useCallback(
- (keys: string[]) => {
- const newSelection = new Set(keys);
-
- if (newSelection.has("all")) {
- // Handle "Select All" behavior
- if (newSelection.size === allValues.length + 1) {
- setSelectedValues(new Set(["all", ...allValues]));
- onChange(name, allValues); // Exclude "all" in the callback
- } else {
- newSelection.delete("all");
- setSelectedValues(newSelection);
- onChange(name, Array.from(newSelection));
- }
- } else {
- setSelectedValues(newSelection);
- onChange(name, Array.from(newSelection));
- }
+ const handleValuesChange = useCallback(
+ (newValues: string[]) => {
+ onChange(name, newValues);
},
- [allValues, name, onChange],
+ [name, onChange],
);
- const handleSelectAllClick = useCallback(() => {
- if (selectedValues.has("all")) {
- setSelectedValues(new Set());
- onChange(name, []);
- } else {
- const newSelection = new Set(["all", ...allValues]);
- setSelectedValues(newSelection);
- onChange(name, allValues);
- }
- }, [allValues, name, onChange, selectedValues]);
-
return (
-
-
-
- }
- size="md"
- >
- {label}
-
-
-
-
-
-
- Select All
-
-
-
- {values.map(({ id, name }) => (
-
- {name}
-
- ))}
-
-
-
-
-
-
- {/* Selected Values Display */}
- {selectedValues.size > 0 && (
-
- {Array.from(selectedValues)
- .filter((value) => value !== "all")
- .map((value) => {
- const selectedItem = values.find((item) => item.id === value);
- return (
-
- {selectedItem?.name || value}
-
- );
- })}
-
- )}
+
+
{label}
+
+
+
+
+
+ {values.map((item) => (
+
+ {item.name}
+
+ ))}
+
+
);
};
diff --git a/ui/components/ui/custom/custom-input.tsx b/ui/components/ui/custom/custom-input.tsx
index 277fae0660..63007362f9 100644
--- a/ui/components/ui/custom/custom-input.tsx
+++ b/ui/components/ui/custom/custom-input.tsx
@@ -21,7 +21,6 @@ interface CustomInputProps
{
defaultValue?: string;
isReadOnly?: boolean;
isRequired?: boolean;
- isInvalid?: boolean;
isDisabled?: boolean;
showFormMessage?: boolean;
}
@@ -40,7 +39,6 @@ export const CustomInput = ({
defaultValue,
isReadOnly = false,
isRequired = true,
- isInvalid,
isDisabled = false,
showFormMessage = true,
}: CustomInputProps) => {
@@ -101,8 +99,8 @@ export const CustomInput = ({
id={name}
classNames={{
label:
- "tracking-tight font-light !text-default-500 text-xs z-0!",
- input: "text-default-500 text-small",
+ "tracking-tight font-light !text-text-neutral-secondary text-xs z-0!",
+ input: "text-text-neutral-secondary text-small",
}}
isRequired={inputIsRequired}
label={inputLabel}
@@ -111,7 +109,6 @@ export const CustomInput = ({
type={inputType}
variant={variant}
size={size}
- isInvalid={isInvalid}
defaultValue={defaultValue}
endContent={endContent}
isDisabled={isDisabled}
@@ -121,7 +118,7 @@ export const CustomInput = ({
/>
{showFormMessage && (
-
+
)}
>
)}
diff --git a/ui/components/ui/custom/custom-link.tsx b/ui/components/ui/custom/custom-link.tsx
index ecf6257d25..27e8442cac 100644
--- a/ui/components/ui/custom/custom-link.tsx
+++ b/ui/components/ui/custom/custom-link.tsx
@@ -33,13 +33,10 @@ export const CustomLink = React.forwardRef(
ref={ref}
href={href}
scroll={scroll}
- className={cn(
- `text-${size} text-primary font-medium text-nowrap break-all decoration-1 hover:underline`,
- className,
- )}
aria-label={ariaLabel}
target={target}
- rel="noopener noreferrer"
+ rel={target === "_blank" ? "noopener noreferrer" : undefined}
+ className={cn(`text-${size} text-button-tertiary p-0`, className)}
{...props}
>
{children}
diff --git a/ui/components/ui/custom/custom-loader.tsx b/ui/components/ui/custom/custom-loader.tsx
deleted file mode 100644
index 4dd67b6e78..0000000000
--- a/ui/components/ui/custom/custom-loader.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-
-export const CustomLoader = (props: { size?: "small" }) => {
- const [loadingSpinner, setloadingSpinner] = useState(0);
- const loadingChars = "|/-\\";
-
- const textClasses = `w-xs px-xs ${props.size === "small" ? "!text-s" : ""}`;
-
- useEffect(() => {
- setTimeout(() => setloadingSpinner(loadingSpinner + 1), 150);
- }, [loadingSpinner]);
-
- return {loadingChars[loadingSpinner % 4]}
;
-};
diff --git a/ui/components/ui/custom/custom-modal-buttons.tsx b/ui/components/ui/custom/custom-modal-buttons.tsx
index de9f1fc629..922452771b 100644
--- a/ui/components/ui/custom/custom-modal-buttons.tsx
+++ b/ui/components/ui/custom/custom-modal-buttons.tsx
@@ -1,6 +1,7 @@
+import { Loader2 } from "lucide-react";
import { ReactNode } from "react";
-import { CustomButton } from "@/components/ui/custom/custom-button";
+import { Button } from "@/components/shadcn";
interface ModalButtonsProps {
onCancel: () => void;
@@ -21,33 +22,32 @@ export const ModalButtons = ({
submitColor = "action",
submitIcon,
}: ModalButtonsProps) => {
+ const submitVariant = submitColor === "danger" ? "destructive" : "default";
+
return (
-
-
+
Cancel
-
-
+
- {submitText}
-
+ {isLoading ? (
+
+ ) : (
+ submitIcon && submitIcon
+ )}
+ {isLoading ? "Loading" : submitText}
+
);
};
diff --git a/ui/components/ui/custom/custom-radio.tsx b/ui/components/ui/custom/custom-radio.tsx
index 5da5b4ab26..5594721ec7 100644
--- a/ui/components/ui/custom/custom-radio.tsx
+++ b/ui/components/ui/custom/custom-radio.tsx
@@ -28,7 +28,7 @@ export const CustomRadio: React.FC = (props) => {
className={cn(
"group tap-highlight-transparent inline-flex flex-row-reverse items-center justify-between hover:opacity-70 active:opacity-50",
"border-default max-w-full cursor-pointer gap-4 rounded-lg border-2 p-4",
- "hover:border-action data-[selected=true]:border-action w-full",
+ "hover:border-button-primary data-[selected=true]:border-button-primary w-full",
)}
>
diff --git a/ui/components/ui/custom/custom-table-link.tsx b/ui/components/ui/custom/custom-table-link.tsx
index 722a7c79f9..c74d306453 100644
--- a/ui/components/ui/custom/custom-table-link.tsx
+++ b/ui/components/ui/custom/custom-table-link.tsx
@@ -1,6 +1,8 @@
"use client";
-import { CustomButton } from "@/components/ui/custom";
+import Link from "next/link";
+
+import { Button } from "@/components/shadcn";
interface TableLinkProps {
href: string;
@@ -9,18 +11,18 @@ interface TableLinkProps {
}
export const TableLink = ({ href, label, isDisabled }: TableLinkProps) => {
+ if (isDisabled) {
+ return (
+
+ {label}
+
+ );
+ }
+
return (
- // TODO: Replace CustomButton with CustomLink once the CustomLink component is merged.
-
- {label}
-
+
+ {label}
+
);
};
diff --git a/ui/components/ui/custom/custom-textarea.tsx b/ui/components/ui/custom/custom-textarea.tsx
index 6d1aeab6f5..e006b65a26 100644
--- a/ui/components/ui/custom/custom-textarea.tsx
+++ b/ui/components/ui/custom/custom-textarea.tsx
@@ -16,7 +16,6 @@ interface CustomTextareaProps {
placeholder?: string;
defaultValue?: string;
isRequired?: boolean;
- isInvalid?: boolean;
minRows?: number;
maxRows?: number;
fullWidth?: boolean;
@@ -34,7 +33,6 @@ export const CustomTextarea = ({
size = "md",
defaultValue,
isRequired = false,
- isInvalid = false,
minRows = 3,
maxRows = 8,
fullWidth = true,
@@ -55,7 +53,6 @@ export const CustomTextarea = ({
placeholder={placeholder}
variant={variant}
size={size}
- isInvalid={isInvalid}
isRequired={isRequired}
defaultValue={defaultValue}
minRows={minRows}
@@ -66,7 +63,7 @@ export const CustomTextarea = ({
{...field}
/>
-
+
>
)}
/>
diff --git a/ui/components/ui/custom/index.ts b/ui/components/ui/custom/index.ts
index b263ce20d7..19e8fef2f4 100644
--- a/ui/components/ui/custom/index.ts
+++ b/ui/components/ui/custom/index.ts
@@ -1,10 +1,9 @@
export * from "./custom-alert-modal";
-export * from "./custom-box";
-export * from "./custom-button";
-export * from "./custom-dropdown-filter";
+export * from "./custom-banner";
export * from "./custom-dropdown-selection";
export * from "./custom-input";
-export * from "./custom-loader";
+export * from "./custom-link";
+export * from "./custom-modal-buttons";
export * from "./custom-radio";
export * from "./custom-section";
export * from "./custom-server-input";
diff --git a/ui/components/ui/dialog/dialog.tsx b/ui/components/ui/dialog/dialog.tsx
index beb7d56d38..845b9f2a33 100644
--- a/ui/components/ui/dialog/dialog.tsx
+++ b/ui/components/ui/dialog/dialog.tsx
@@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
- onDownload(paramId)}
- className="text-default-500 hover:text-primary p-0 disabled:opacity-30"
- isIconOnly
- ariaLabel={ariaLabel}
- size="sm"
+ size="icon-sm"
+ disabled={isDisabled || isDownloading}
+ onClick={() => onDownload(paramId)}
+ aria-label={ariaLabel}
+ className="p-0 disabled:opacity-30"
>
-
+
);
diff --git a/ui/components/ui/dropdown-menu/dropdown-menu.tsx b/ui/components/ui/dropdown-menu/dropdown-menu.tsx
index b796f903fe..7d27f09a7c 100644
--- a/ui/components/ui/dropdown-menu/dropdown-menu.tsx
+++ b/ui/components/ui/dropdown-menu/dropdown-menu.tsx
@@ -51,7 +51,7 @@ const DropdownMenuSubContent = React.forwardRef<
= ({
@@ -22,6 +23,7 @@ export const EntityInfoShort: React.FC = ({
entityId,
hideCopyButton = false,
showConnectionStatus = false,
+ maxWidth = "max-w-[120px]",
}) => {
return (
@@ -41,7 +43,7 @@ export const EntityInfoShort: React.FC = ({
)}
-
+
{entityAlias && (
@@ -52,7 +54,7 @@ export const EntityInfoShort: React.FC = ({
}
+ icon={ }
/>
diff --git a/ui/components/ui/entities/info-field.tsx b/ui/components/ui/entities/info-field.tsx
index b49c3ee83f..a9d51045fa 100644
--- a/ui/components/ui/entities/info-field.tsx
+++ b/ui/components/ui/entities/info-field.tsx
@@ -31,32 +31,32 @@ export const InfoField = ({
if (inline) {
return (
-
+
{label}:
{tooltipContent && (
-
+
)}
- {children}
+ {children}
);
}
return (
-
+
{label}
{tooltipContent && (
-
+
)}
@@ -64,13 +64,13 @@ export const InfoField = ({
{variant === "simple" ? (
-
+
{children}
) : variant === "transparent" ? (
-
{children}
+
{children}
) : (
-
+
{children}
)}
diff --git a/ui/components/ui/entities/snippet-chip.tsx b/ui/components/ui/entities/snippet-chip.tsx
index 5e0b7a4d1c..04dabfde67 100644
--- a/ui/components/ui/entities/snippet-chip.tsx
+++ b/ui/components/ui/entities/snippet-chip.tsx
@@ -28,11 +28,9 @@ export const SnippetChip = ({
classNames={{
content: "min-w-0 overflow-hidden",
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
+ base: "border-border-neutral-tertiary bg-bg-neutral-tertiary rounded-lg border py-1",
}}
- color="default"
size="sm"
- variant="flat"
- radius="lg"
hideSymbol
copyIcon={
}
checkIcon={
}
diff --git a/ui/components/ui/feedback-banner/feedback-banner.tsx b/ui/components/ui/feedback-banner/feedback-banner.tsx
index 806761e5ed..a774cfffac 100644
--- a/ui/components/ui/feedback-banner/feedback-banner.tsx
+++ b/ui/components/ui/feedback-banner/feedback-banner.tsx
@@ -17,7 +17,7 @@ const typeStyles: Record<
error: {
border: "border-danger",
bg: "bg-system-error-light/30 dark:bg-system-error-light/80",
- text: "text-danger",
+ text: "text-text-error",
},
warning: {
border: "border-warning",
diff --git a/ui/components/ui/form/Form.tsx b/ui/components/ui/form/Form.tsx
index 674a8c4a39..f4cc7d5403 100644
--- a/ui/components/ui/form/Form.tsx
+++ b/ui/components/ui/form/Form.tsx
@@ -100,7 +100,7 @@ const FormLabel = React.forwardRef<
return (
@@ -141,10 +141,7 @@ const FormDescription = React.forwardRef<
);
@@ -167,7 +164,7 @@ const FormMessage = React.forwardRef<
ref={ref}
id={formMessageId}
className={cn(
- "text-[0.8rem] font-medium text-red-500 dark:text-red-900",
+ "text-text-error max-w-full text-xs font-medium",
className,
)}
{...props}
diff --git a/ui/components/ui/form/form-buttons.tsx b/ui/components/ui/form/form-buttons.tsx
index ceaa87a05f..0e8598c9c0 100644
--- a/ui/components/ui/form/form-buttons.tsx
+++ b/ui/components/ui/form/form-buttons.tsx
@@ -1,11 +1,11 @@
"use client";
+import { Loader2 } from "lucide-react";
import { Dispatch, SetStateAction } from "react";
import { useFormStatus } from "react-dom";
import { SaveIcon } from "@/components/icons";
-
-import { CustomButton } from "../custom";
+import { Button } from "@/components/shadcn";
interface FormCancelButtonProps {
setIsOpen?: Dispatch
>;
@@ -56,18 +56,10 @@ const FormCancelButton = ({
};
return (
-
- {children}
-
+
+ {leftIcon}
+ {children}
+
);
};
@@ -79,22 +71,18 @@ const FormSubmitButton = ({
rightIcon,
}: FormSubmitButtonProps) => {
const { pending } = useFormStatus();
+ const submitVariant = color === "danger" ? "destructive" : "default";
return (
-
- {pending ? <>{loadingText}> : {children} }
-
+ {pending ? : rightIcon}
+ {pending ? loadingText : children}
+
);
};
@@ -110,7 +98,7 @@ export const FormButtons = ({
leftIcon,
}: FormButtonsProps) => {
return (
-
+
= ({
+export const NavigationHeader = ({
title,
icon,
href,
-}) => {
+}: NavigationHeaderProps) => {
return (
<>
diff --git a/ui/components/ui/index.ts b/ui/components/ui/index.ts
index cfa3efb5f2..1202e199b7 100644
--- a/ui/components/ui/index.ts
+++ b/ui/components/ui/index.ts
@@ -3,7 +3,6 @@ export * from "./action-card/ActionCard";
export * from "./alert/Alert";
export * from "./alert-dialog/AlertDialog";
export * from "./breadcrumbs";
-export * from "./chart/Chart";
export * from "./collapsible/collapsible";
export * from "./content-layout/content-layout";
export * from "./dialog/dialog";
diff --git a/ui/components/ui/main-layout/main-layout.tsx b/ui/components/ui/main-layout/main-layout.tsx
index f5e00259c4..26a7cb2aca 100644
--- a/ui/components/ui/main-layout/main-layout.tsx
+++ b/ui/components/ui/main-layout/main-layout.tsx
@@ -14,11 +14,29 @@ export default function MainLayout({
if (!sidebar) return null;
const { getOpenState, settings } = sidebar;
return (
-
+
+ {/* Top-left gradient halo */}
+
+
+ {/* Bottom-right gradient halo */}
+
+
diff --git a/ui/components/ui/nav-bar/navbar-client.tsx b/ui/components/ui/nav-bar/navbar-client.tsx
index 259ff1fc03..fa48c9c103 100644
--- a/ui/components/ui/nav-bar/navbar-client.tsx
+++ b/ui/components/ui/nav-bar/navbar-client.tsx
@@ -6,22 +6,29 @@ import { ReactNode } from "react";
import { Button } from "@/components/shadcn";
import { ThemeSwitch } from "@/components/ThemeSwitch";
import { BreadcrumbNavigation } from "@/components/ui";
+import { useSidebar } from "@/hooks/use-sidebar";
import { SheetMenu } from "../sidebar/sheet-menu";
+import { SidebarToggle } from "../sidebar/sidebar-toggle";
import { UserNav } from "../user-nav/user-nav";
interface NavbarClientProps {
title: string;
- icon: string | ReactNode;
+ icon?: string | ReactNode;
feedsSlot?: ReactNode;
}
export function NavbarClient({ title, icon, feedsSlot }: NavbarClientProps) {
+ const { isOpen, toggleOpen } = useSidebar();
+
return (
-
+
+
+
+
diff --git a/ui/components/ui/nav-bar/navbar.tsx b/ui/components/ui/nav-bar/navbar.tsx
index 1496cd02b8..95cc35d48b 100644
--- a/ui/components/ui/nav-bar/navbar.tsx
+++ b/ui/components/ui/nav-bar/navbar.tsx
@@ -6,7 +6,7 @@ import { FeedsLoadingFallback, NavbarClient } from "./navbar-client";
interface NavbarProps {
title: string;
- icon: string | ReactNode;
+ icon?: string | ReactNode;
}
export function Navbar({ title, icon }: NavbarProps) {
diff --git a/ui/components/ui/sheet/sheet.tsx b/ui/components/ui/sheet/sheet.tsx
index 5d6cccf9ef..49ba1e7f92 100644
--- a/ui/components/ui/sheet/sheet.tsx
+++ b/ui/components/ui/sheet/sheet.tsx
@@ -31,16 +31,16 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
- "fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-neutral-950 dark:border-prowler-blue-800",
+ "fixed z-50 gap-4 border border-border-neutral-secondary bg-bg-neutral-secondary p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
- top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ top: "inset-x-0 top-0 rounded-b-xl data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
- "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
- left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
+ "inset-x-0 bottom-0 rounded-t-xl data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 rounded-r-xl data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
right:
- "inset-y-0 right-0 h-full w-3/4 border-t border-b border-l-2 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
+ "inset-y-0 right-0 h-full w-3/4 rounded-l-xl data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
},
},
defaultVariants: {
diff --git a/ui/components/ui/sheet/trigger-sheet.tsx b/ui/components/ui/sheet/trigger-sheet.tsx
index fb51a65c47..7d302c24c7 100644
--- a/ui/components/ui/sheet/trigger-sheet.tsx
+++ b/ui/components/ui/sheet/trigger-sheet.tsx
@@ -31,7 +31,7 @@ export function TriggerSheet({
{triggerComponent}
-
+
{title}
{description}
diff --git a/ui/components/ui/sidebar/collapse-menu-button.tsx b/ui/components/ui/sidebar/collapse-menu-button.tsx
deleted file mode 100644
index 3a2f31419f..0000000000
--- a/ui/components/ui/sidebar/collapse-menu-button.tsx
+++ /dev/null
@@ -1,278 +0,0 @@
-"use client";
-
-import { Tooltip } from "@heroui/tooltip";
-import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu";
-import { ChevronDown } from "lucide-react";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-import { useState } from "react";
-
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "@/components/ui/collapsible/collapsible";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu/dropdown-menu";
-import { cn } from "@/lib/utils";
-import { CollapseMenuButtonProps } from "@/types";
-
-import { Button } from "../button/button";
-
-export const CollapseMenuButton = ({
- icon: Icon,
- label,
- submenus,
- defaultOpen,
- isOpen,
-}: CollapseMenuButtonProps) => {
- const pathname = usePathname();
- const isSubmenuActive = submenus.some((submenu) =>
- submenu.active === undefined ? submenu.href === pathname : submenu.active,
- );
- const [isCollapsed, setIsCollapsed] = useState(
- isSubmenuActive || defaultOpen,
- );
-
- return isOpen ? (
-
-
-
-
-
-
-
- {submenus.map(
- (
- { href, label, active, icon: SubIcon, target, disabled, onClick },
- index,
- ) => {
- const isActive =
- (active === undefined && pathname === href) || active;
-
- if (disabled && label === "Mutelist") {
- return (
-
-
-
- );
- }
-
- return (
-
-
-
-
-
-
-
- {label}
-
-
-
- );
- },
- )}
-
-
- ) : (
-
-
-
-
-
-
-
-
-
-
- {label}
-
-
- {submenus.map(
- (
- { href, label, active, icon: SubIcon, disabled, onClick },
- index,
- ) => {
- const isActive =
- (active === undefined && pathname === href) || active;
-
- if (disabled && label === "Mutelist") {
- return (
-
-
-
- );
- }
-
- return (
-
- {disabled ? (
-
- ) : (
-
-
- {label}
-
- )}
-
- );
- },
- )}
-
-
-
- );
-};
diff --git a/ui/components/ui/sidebar/collapsible-menu.tsx b/ui/components/ui/sidebar/collapsible-menu.tsx
new file mode 100644
index 0000000000..7937bf940d
--- /dev/null
+++ b/ui/components/ui/sidebar/collapsible-menu.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { ChevronDown } from "lucide-react";
+import { usePathname } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import { Button } from "@/components/shadcn/button/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible/collapsible";
+import { SubmenuItem } from "@/components/ui/sidebar/submenu-item";
+import { cn } from "@/lib/utils";
+import { IconComponent, SubmenuProps } from "@/types";
+
+interface CollapsibleMenuProps {
+ icon: IconComponent;
+ label: string;
+ submenus: SubmenuProps[];
+ defaultOpen?: boolean;
+ isOpen: boolean;
+}
+
+export const CollapsibleMenu = ({
+ icon: Icon,
+ label,
+ submenus,
+ defaultOpen = false,
+ isOpen: isSidebarOpen,
+}: CollapsibleMenuProps) => {
+ const pathname = usePathname();
+ const isSubmenuActive = submenus.some((submenu) =>
+ submenu.active === undefined ? submenu.href === pathname : submenu.active,
+ );
+ const [isCollapsed, setIsCollapsed] = useState(
+ isSubmenuActive || defaultOpen,
+ );
+
+ // Collapse the menu when sidebar is closed
+ useEffect(() => {
+ if (!isSidebarOpen) {
+ setIsCollapsed(false);
+ }
+ }, [isSidebarOpen]);
+
+ return (
+
+
+
+
+
+ {isSidebarOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {!isSidebarOpen && (
+ {label}
+ )}
+
+
+ {submenus.map((submenu, index) => (
+
+ ))}
+
+
+ );
+};
diff --git a/ui/components/ui/sidebar/index.ts b/ui/components/ui/sidebar/index.ts
index 166c6083cf..6fe9b4c4e2 100644
--- a/ui/components/ui/sidebar/index.ts
+++ b/ui/components/ui/sidebar/index.ts
@@ -1,5 +1,7 @@
-export * from "./collapse-menu-button";
+export * from "./collapsible-menu";
export * from "./menu";
+export * from "./menu-item";
export * from "./sheet-menu";
export * from "./sidebar";
export * from "./sidebar-toggle";
+export * from "./submenu-item";
diff --git a/ui/components/ui/sidebar/menu-item.tsx b/ui/components/ui/sidebar/menu-item.tsx
new file mode 100644
index 0000000000..6d6480a6e6
--- /dev/null
+++ b/ui/components/ui/sidebar/menu-item.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { Button } from "@/components/shadcn/button/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
+import { cn } from "@/lib/utils";
+import { IconComponent } from "@/types";
+
+interface MenuItemProps {
+ href: string;
+ label: string;
+ icon: IconComponent;
+ active?: boolean;
+ target?: string;
+ tooltip?: string;
+ isOpen: boolean;
+}
+
+export const MenuItem = ({
+ href,
+ label,
+ icon: Icon,
+ active,
+ target,
+ tooltip,
+ isOpen,
+}: MenuItemProps) => {
+ const pathname = usePathname();
+ const isActive = active !== undefined ? active : pathname.startsWith(href);
+
+ // Show tooltip always for Prowler Hub, or when sidebar is collapsed
+ const showTooltip = label === "Prowler Hub" ? !!tooltip : !isOpen;
+
+ return (
+
+
+
+
+
+
+
+
+ {isOpen &&
{label}
}
+
+
+
+
+ {showTooltip && (
+ {tooltip || label}
+ )}
+
+ );
+};
diff --git a/ui/components/ui/sidebar/menu.tsx b/ui/components/ui/sidebar/menu.tsx
index 4047db376c..b515aed27f 100644
--- a/ui/components/ui/sidebar/menu.tsx
+++ b/ui/components/ui/sidebar/menu.tsx
@@ -1,19 +1,19 @@
"use client";
import { Divider } from "@heroui/divider";
-import { Ellipsis, LogOut } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { logOut } from "@/actions/auth";
import { AddIcon, InfoIcon } from "@/components/icons";
-import { CollapseMenuButton } from "@/components/ui/sidebar/collapse-menu-button";
+import { Button } from "@/components/shadcn/button/button";
import {
Tooltip,
TooltipContent,
- TooltipProvider,
TooltipTrigger,
-} from "@/components/ui/tooltip/tooltip";
+} from "@/components/shadcn/tooltip";
+import { ScrollArea } from "@/components/ui/scroll-area/scroll-area";
+import { CollapsibleMenu } from "@/components/ui/sidebar/collapsible-menu";
+import { MenuItem } from "@/components/ui/sidebar/menu-item";
import { useAuth } from "@/hooks";
import { getMenuList } from "@/lib/menu-list";
import { cn } from "@/lib/utils";
@@ -21,16 +21,11 @@ import { useUIStore } from "@/store/ui/store";
import { GroupProps } from "@/types";
import { RolePermissionAttributes } from "@/types/users";
-import { Button } from "../button/button";
-import { CustomButton } from "../custom/custom-button";
-import { ScrollArea } from "../scroll-area/scroll-area";
-
interface MenuHideRule {
label: string;
condition: (permissions: RolePermissionAttributes) => boolean;
}
-// Configuration for hiding menu items based on permissions
const MENU_HIDE_RULES: MenuHideRule[] = [
{
label: "Billing",
@@ -40,30 +35,22 @@ const MENU_HIDE_RULES: MenuHideRule[] = [
label: "Integrations",
condition: (permissions) => permissions?.manage_integrations === false,
},
- // Add more rules as needed:
- // {
- // label: "Users",
- // condition: (permissions) => !permissions?.manage_users
- // },
- // {
- // label: "Configuration",
- // condition: (permissions) => !permissions?.manage_providers
- // },
];
-const hideMenuItems = (menuGroups: GroupProps[], labelsToHide: string[]) => {
- return menuGroups.map((group) => ({
- ...group,
- menus: group.menus
- .filter((menu) => !labelsToHide.includes(menu.label))
- .map((menu) => ({
- ...menu,
- submenus:
- menu.submenus?.filter(
+const filterMenus = (menuGroups: GroupProps[], labelsToHide: string[]) => {
+ return menuGroups
+ .map((group) => ({
+ ...group,
+ menus: group.menus
+ .filter((menu) => !labelsToHide.includes(menu.label))
+ .map((menu) => ({
+ ...menu,
+ submenus: menu.submenus?.filter(
(submenu) => !labelsToHide.includes(submenu.label),
- ) || [],
- })),
- }));
+ ),
+ })),
+ }))
+ .filter((group) => group.menus.length > 0);
};
export const Menu = ({ isOpen }: { isOpen: boolean }) => {
@@ -71,6 +58,7 @@ export const Menu = ({ isOpen }: { isOpen: boolean }) => {
const { permissions } = useAuth();
const { hasProviders, openMutelistModal, requestMutelistModalOpen } =
useUIStore();
+
const menuList = getMenuList({
pathname,
hasProviders,
@@ -82,175 +70,106 @@ export const Menu = ({ isOpen }: { isOpen: boolean }) => {
rule.condition(permissions),
).map((rule) => rule.label);
- const filteredMenuList = hideMenuItems(menuList, labelsToHide);
+ const filteredMenuList = filterMenus(menuList, labelsToHide);
return (
- <>
-
-
: null}
- >
- {isOpen ? "Launch Scan" :
}
-
-
-
-
-
- {filteredMenuList.map(({ groupLabel, menus }, index) => (
-
- {(menus.length > 0 && isOpen && groupLabel) ||
- isOpen === undefined ? (
-
- {groupLabel}
-
- ) : !isOpen && isOpen !== undefined && groupLabel ? (
-
-
-
-
-
-
-
-
- {groupLabel}
-
-
-
- ) : (
-
- )}
- {menus.map((menu, index) => {
- const {
- href,
- label,
- icon: Icon,
- active,
- submenus,
- defaultOpen,
- target,
- tooltip,
- } = menu;
- return !submenus || submenus.length === 0 ? (
-
-
-
-
-
-
-
-
-
-
- {label}
-
-
-
-
- {tooltip && (
-
- {tooltip}
-
- )}
-
-
-
- ) : (
-
-
-
- );
- })}
-
- ))}
-
-
-
-
-
-
-
- logOut()}
- variant="outline"
- className="mt-5 h-10 w-full justify-center"
- >
-
-
-
-
- Sign out
-
-
-
- {isOpen === false && (
- Sign out
- )}
-
-
+
+ {/* Launch Scan Button */}
+
+
+
+
+
+ {isOpen ? "Launch Scan" : }
+
+
+
+ {!isOpen && Launch Scan }
+
-
-
{process.env.NEXT_PUBLIC_PROWLER_RELEASE_VERSION}
- {process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && (
+ {/* Menu Items */}
+
+
+ {/* Footer */}
+
+ {isOpen ? (
<>
-
-
-
-
- Service Status
-
-
+
{process.env.NEXT_PUBLIC_PROWLER_RELEASE_VERSION}
+ {process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && (
+ <>
+
+
+
+
+ Service Status
+
+
+ >
+ )}
>
+ ) : (
+ process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true" && (
+
+
+
+
+
+
+ Service Status
+
+ )
)}
- >
+
);
};
diff --git a/ui/components/ui/sidebar/sheet-menu.tsx b/ui/components/ui/sidebar/sheet-menu.tsx
index e66fc06abd..05a3ebb09d 100644
--- a/ui/components/ui/sidebar/sheet-menu.tsx
+++ b/ui/components/ui/sidebar/sheet-menu.tsx
@@ -22,10 +22,7 @@ export function SheetMenu() {
-
+
Sidebar
diff --git a/ui/components/ui/sidebar/sidebar-toggle.tsx b/ui/components/ui/sidebar/sidebar-toggle.tsx
index ba8e209840..c479aab84a 100644
--- a/ui/components/ui/sidebar/sidebar-toggle.tsx
+++ b/ui/components/ui/sidebar/sidebar-toggle.tsx
@@ -1,8 +1,15 @@
-import { ChevronLeft } from "lucide-react";
-
-import { cn } from "@/lib/utils";
+import {
+ SidebarCollapseIcon,
+ SidebarExpandIcon,
+} from "@/components/icons/Icons";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/shadcn/tooltip";
import { Button } from "../button/button";
+
interface SidebarToggleProps {
isOpen: boolean | undefined;
setIsOpen?: () => void;
@@ -10,20 +17,24 @@ interface SidebarToggleProps {
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
return (
-
- setIsOpen?.()}
- className="h-8 w-8 rounded-md"
- variant="outline"
- size="icon"
- >
-
+
+ setIsOpen?.()}
+ className="h-8 w-8 rounded-md"
+ variant="outline"
+ size="icon"
+ >
+ {isOpen === false ? (
+
+ ) : (
+
)}
- />
-
-
+
+
+
+ {isOpen ? "Collapse Sidebar" : "Expand Sidebar"}
+
+
);
}
diff --git a/ui/components/ui/sidebar/sidebar.tsx b/ui/components/ui/sidebar/sidebar.tsx
index 7a0c8c95c3..6afd6c87eb 100644
--- a/ui/components/ui/sidebar/sidebar.tsx
+++ b/ui/components/ui/sidebar/sidebar.tsx
@@ -11,12 +11,11 @@ import { cn } from "@/lib/utils";
import { Button } from "../button/button";
import { Menu } from "./menu";
-import { SidebarToggle } from "./sidebar-toggle";
export function Sidebar() {
const sidebar = useStore(useSidebar, (x) => x);
if (!sidebar) return null;
- const { isOpen, toggleOpen, getOpenState, setIsHover, settings } = sidebar;
+ const { isOpen, getOpenState, setIsHover, settings } = sidebar;
return (