diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 31de233c08..bc87128158 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Customer Support menu item [(#9143)](https://github.com/prowler-cloud/prowler/pull/9143) - IaC (Infrastructure as Code) provider support for scanning remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751) - External resource link to IaC findings for direct navigation to source code in Git repositories [(#9151)](https://github.com/prowler-cloud/prowler/pull/9151) +- New Overview page and new app styles [(#9234)](https://github.com/prowler-cloud/prowler/pull/9234) ### 🔄 Changed diff --git a/ui/actions/overview/overview.ts b/ui/actions/overview/overview.ts index 3d379e61c6..fec6764ee1 100644 --- a/ui/actions/overview/overview.ts +++ b/ui/actions/overview/overview.ts @@ -123,7 +123,7 @@ export const getThreatScore = async ({ } = {}) => { const headers = await getAuthHeaders({ contentType: false }); - const url = new URL(`${apiBaseUrl}/overviews/threat-score`); + const url = new URL(`${apiBaseUrl}/overviews/threatscore`); // Handle multiple filters Object.entries(filters).forEach(([key, value]) => { diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 2b7cfd8cfd..d94b700b33 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -1,6 +1,5 @@ import { Spacer } from "@heroui/spacer"; -import Image from "next/image"; -import React, { Suspense } from "react"; +import { Suspense } from "react"; import { getComplianceAttributes, @@ -8,16 +7,15 @@ import { getComplianceRequirements, } from "@/actions/compliances"; import { - BarChart, - BarChartSkeleton, ClientAccordionWrapper, ComplianceHeader, - ComplianceScanInfo, - HeatmapChart, - HeatmapChartSkeleton, - PieChart, - PieChartSkeleton, + RequirementsStatusCard, + RequirementsStatusCardSkeleton, + // SectionsFailureRateCard, + // SectionsFailureRateCardSkeleton, SkeletonAccordion, + TopFailedSectionsCard, + TopFailedSectionsCardSkeleton, } from "@/components/compliance"; import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance"; import { ContentLayout } from "@/components/ui"; @@ -42,38 +40,6 @@ interface ComplianceDetailSearchParams { pageSize?: string; } -const ComplianceIconSmall = ({ - logoPath, - title, -}: { - logoPath: string; - title: string; -}) => { - return ( -
- {`${title} -
- ); -}; - -const ChartsWrapper = ({ - children, -}: { - children: React.ReactNode; - logoPath?: string; -}) => { - return ( -
- {children} -
- ); -}; - export default async function ComplianceDetail({ params, searchParams, @@ -98,8 +64,8 @@ export default async function ComplianceDetail({ const formattedTitle = compliancetitle.split("-").join(" "); const pageTitle = version - ? `Compliance Details: ${formattedTitle} - ${version}` - : `Compliance Details: ${formattedTitle}`; + ? `${formattedTitle} - ${version}` + : `${formattedTitle}`; let selectedScan: ScanEntity | null = null; @@ -122,57 +88,37 @@ export default async function ComplianceDetail({ // Use compliance_name from attributes if available, otherwise fallback to formatted title const complianceName = attributesData?.data?.[0]?.attributes?.compliance_name; - const finalPageTitle = complianceName - ? `Compliance Details: ${complianceName}` - : pageTitle; + const finalPageTitle = complianceName ? `${complianceName}` : pageTitle; return ( - - ) : ( - "fluent-mdl2:compliance-audit" - ) - } - > - {selectedScanId && selectedScan && ( -
-
- + + + {attributesData?.data?.[0]?.attributes?.framework === + "ProwlerThreatScore" && + selectedScanId && ( +
+
- -
- )} -
-
- -
- {attributesData?.data?.[0]?.attributes?.framework === - "ProwlerThreatScore" && - selectedScanId && ( -
- -
- )} -
+ )} - - - - - +
+ + + {/* */} +
} @@ -182,7 +128,6 @@ export default async function ComplianceDetail({ scanId={selectedScanId || ""} region={regionFilter} filter={cisProfileFilter} - logoPath={logoPath} attributesData={attributesData} /> @@ -195,14 +140,12 @@ const SSRComplianceContent = async ({ scanId, region, filter, - logoPath, attributesData, }: { complianceId: string; scanId: string; region?: string; filter?: string; - logoPath?: string; attributesData: AttributesData; }) => { const requirementsData = await getComplianceRequirements({ @@ -215,11 +158,11 @@ const SSRComplianceContent = async ({ if (!scanId || type === "tasks") { return (
- - - - - +
+ + + {/* */} +
); @@ -232,7 +175,7 @@ const SSRComplianceContent = async ({ requirementsData, filter, ); - const categoryHeatmapData = mapper.calculateCategoryHeatmapData(data); + // const categoryHeatmapData = mapper.calculateCategoryHeatmapData(data); const totalRequirements: RequirementsTotals = data.reduce( (acc: RequirementsTotals, framework: Framework) => ({ pass: acc.pass + framework.pass, @@ -246,17 +189,17 @@ const SSRComplianceContent = async ({ return (
- - + - - - + + {/* */} +
- + - } - onPress={handleDownload} - isLoading={isDownloading} + variant="default" size="sm" + onClick={handleDownload} + disabled={isDownloading} > + PDF ThreatScore Report ); diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx index a4bde8104f..d230a1bc41 100644 --- a/ui/app/(prowler)/compliance/page.tsx +++ b/ui/app/(prowler)/compliance/page.tsx @@ -1,4 +1,3 @@ -export const dynamic = "force-dynamic"; import { Suspense } from "react"; import { @@ -138,7 +137,7 @@ export default async function Compliance({ {selectedScanId ? ( <>
-
+
- - - - {is500Error - ? "Server temporarily unavailable" - : "An unexpected error occurred"} - - - {is500Error - ? "The server is experiencing issues. Our team has been notified and is working on it. Please try again in a few moments." - : "We're sorry for the inconvenience. Please try again or contact support if the problem persists."} - -
- } - ariaLabel="Try Again" - > - Try Again - - - Go to Overview - -
-
+ + +
+ +
+ + {is500Error + ? "Server temporarily unavailable" + : "An unexpected error occurred"} + + + {is500Error + ? "The server is experiencing issues. Our team has been notified and is working on it. Please try again in a few moments." + : "We're sorry for the inconvenience. Please try again or contact support if the problem persists."} + +
+
+
+ +
+ + + Go to Overview + +
+
+
); } diff --git a/ui/app/(prowler)/integrations/amazon-s3/page.tsx b/ui/app/(prowler)/integrations/amazon-s3/page.tsx index f7bd324029..48e74f8baf 100644 --- a/ui/app/(prowler)/integrations/amazon-s3/page.tsx +++ b/ui/app/(prowler)/integrations/amazon-s3/page.tsx @@ -3,6 +3,7 @@ import React from "react"; import { getIntegrations } from "@/actions/integrations"; import { getProviders } from "@/actions/providers"; import { S3IntegrationsManager } from "@/components/integrations/s3/s3-integrations-manager"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; import { ContentLayout } from "@/components/ui"; interface S3IntegrationsProps { @@ -62,29 +63,31 @@ export default async function S3Integrations({ results to S3 buckets.

-
-

- Features: -

-
    -
  • - - Automated scan result exports -
  • -
  • - - Multi-Cloud support -
  • -
  • - - Configurable export paths -
  • -
  • - - IAM role and static credentials -
  • -
-
+ + + Features + + +
    +
  • + + Automated scan result exports +
  • +
  • + + Multi-Cloud support +
  • +
  • + + Configurable export paths +
  • +
  • + + IAM role and static credentials +
  • +
+
+
-
-

- Features: -

-
    -
  • - - Automated findings export -
  • -
  • - - Multi-region support -
  • -
  • - - Send failed findings only -
  • -
  • - - Archive previous findings -
  • -
-
+ + + Features + + +
    +
  • + + Automated findings export +
  • +
  • + + Multi-region support +
  • +
  • + + Send failed findings only +
  • +
  • + + Archive previous findings +
  • +
+
+
-
-

- Features: -

-
    -
  • - - Automated issue creation -
  • -
  • - - Multi-Cloud support -
  • -
  • - - Flexible issue tracking -
  • -
  • - - Project-specific configuration -
  • -
-
+ + + Features + + +
    +
  • + + Automated issue creation +
  • +
  • + + Multi-Cloud support +
  • +
  • + + Flexible issue tracking +
  • +
  • + + Project-specific configuration +
  • +
+
+
- - - - + +
+ + + +
}> diff --git a/ui/app/(prowler)/lighthouse/config/(connect-llm)/layout.tsx b/ui/app/(prowler)/lighthouse/config/(connect-llm)/layout.tsx index 07c15a5a50..fa058f9c09 100644 --- a/ui/app/(prowler)/lighthouse/config/(connect-llm)/layout.tsx +++ b/ui/app/(prowler)/lighthouse/config/(connect-llm)/layout.tsx @@ -13,8 +13,9 @@ import { } from "@/actions/lighthouse/lighthouse"; import { DeleteLLMProviderForm } from "@/components/lighthouse/forms/delete-llm-provider-form"; import { WorkflowConnectLLM } from "@/components/lighthouse/workflow"; +import { Button } from "@/components/shadcn"; import { NavigationHeader } from "@/components/ui"; -import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { CustomAlertModal } from "@/components/ui/custom"; import type { LighthouseProvider } from "@/types/lighthouse"; interface ConnectLLMLayoutProps { @@ -89,33 +90,28 @@ export default function ConnectLLMLayout({ children }: ConnectLLMLayoutProps) { <>
{!isDefaultProvider && ( - - } - onPress={handleSetDefault} + onClick={handleSetDefault} className="w-full sm:w-auto" > + Set as Default - + )} - - } - onPress={() => setIsDeleteOpen(true)} + onClick={() => setIsDeleteOpen(true)} className="w-full sm:w-auto" > + Delete Provider - +
diff --git a/ui/app/(prowler)/new-overview/components/accounts-selector.tsx b/ui/app/(prowler)/new-overview/components/accounts-selector.tsx index 210e1f6f6d..d878712d60 100644 --- a/ui/app/(prowler)/new-overview/components/accounts-selector.tsx +++ b/ui/app/(prowler)/new-overview/components/accounts-selector.tsx @@ -14,12 +14,12 @@ import { OracleCloudProviderBadge, } from "@/components/icons/providers-badge"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/shadcn"; + MultiSelect, + MultiSelectContent, + MultiSelectItem, + MultiSelectTrigger, + MultiSelectValue, +} from "@/components/shadcn/select/multiselect"; import type { ProviderProps, ProviderType } from "@/types/providers"; const PROVIDER_ICON: Record = { @@ -91,7 +91,7 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { }; const selectedLabel = () => { - if (selectedIds.length === 0) return null; // placeholder visible + if (selectedIds.length === 0) return null; if (selectedIds.length === 1) { const p = providers.find((pr) => pr.id === selectedIds[0]); const name = p ? p.attributes.alias || p.attributes.uid : selectedIds[0]; @@ -117,18 +117,14 @@ export function AccountsSelector({ providers }: AccountsSelectorProps) { Filter by cloud provider account. {filterDescription}. Select one or more accounts to view findings. - + + ); } diff --git a/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx b/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx index a4c3da7b14..7dd0504deb 100644 --- a/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx +++ b/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx @@ -4,12 +4,12 @@ import { useRouter, useSearchParams } from "next/navigation"; import { lazy, Suspense } from "react"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/shadcn"; + MultiSelect, + MultiSelectContent, + MultiSelectItem, + MultiSelectTrigger, + MultiSelectValue, +} from "@/components/shadcn/select/multiselect"; import { type ProviderProps, ProviderType } from "@/types/providers"; const AWSProviderBadge = lazy(() => @@ -147,7 +147,7 @@ export const ProviderTypeSelector = ({ }; const selectedLabel = () => { - if (selectedTypes.length === 0) return null; // placeholder visible + if (selectedTypes.length === 0) return null; if (selectedTypes.length === 1) { const providerType = selectedTypes[0] as ProviderType; return ( @@ -174,39 +174,36 @@ export const ProviderTypeSelector = ({ Filter by cloud provider type. Select one or more providers to view findings. - + + ); }; diff --git a/ui/app/(prowler)/new-overview/page.tsx b/ui/app/(prowler)/new-overview/page.tsx index 16b2144e3f..17a17b8929 100644 --- a/ui/app/(prowler)/new-overview/page.tsx +++ b/ui/app/(prowler)/new-overview/page.tsx @@ -22,6 +22,11 @@ export default async function NewOverviewPage({ }: { searchParams: Promise; }) { + //if cloud env throw a 500 err + if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") { + throw new Error("500"); + } + const resolvedSearchParams = await searchParams; const providersData = await getProviders({ page: 1, pageSize: 200 }); diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index 29ebeb06bf..d382dc57a6 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -2,22 +2,8 @@ import { Spacer } from "@heroui/spacer"; import { Suspense } from "react"; import { getLatestFindings } from "@/actions/findings/findings"; -import { - getFindingsBySeverity, - getFindingsByStatus, - getProvidersOverview, -} from "@/actions/overview/overview"; -import { FilterControls } from "@/components/filters"; -import { LighthouseBanner } from "@/components/lighthouse"; -import { - FindingsBySeverityChart, - FindingsByStatusChart, - LinkToFindings, - ProvidersOverview, - SkeletonFindingsBySeverityChart, - SkeletonFindingsByStatusChart, - SkeletonProvidersOverview, -} from "@/components/overview"; +import { getProviders } from "@/actions/providers"; +import { LinkToFindings } from "@/components/overview"; import { ColumnNewFindingsToDate } from "@/components/overview/new-findings-table/table/column-new-findings-to-date"; import { SkeletonTableNewFindings } from "@/components/overview/new-findings-table/table/skeleton-table-new-findings"; import { ContentLayout } from "@/components/ui"; @@ -25,6 +11,20 @@ import { DataTable } from "@/components/ui/table"; import { createDict } from "@/lib/helper"; import { FindingProps, SearchParamsProps } from "@/types"; +import { LighthouseBanner } from "../../components/lighthouse/banner"; +import { AccountsSelector } from "./new-overview/components/accounts-selector"; +import { CheckFindingsSSR } from "./new-overview/components/check-findings"; +import { ProviderTypeSelector } from "./new-overview/components/provider-type-selector"; +import { + RiskSeverityChartSkeleton, + RiskSeverityChartSSR, +} from "./new-overview/components/risk-severity-chart"; +import { StatusChartSkeleton } from "./new-overview/components/status-chart"; +import { + ThreatScoreSkeleton, + ThreatScoreSSR, +} from "./new-overview/components/threat-score"; + const FILTER_PREFIX = "filter["; // Extract only query params that start with "filter[" for API calls @@ -44,98 +44,39 @@ export default async function Home({ }) { const resolvedSearchParams = await searchParams; const searchParamsKey = JSON.stringify(resolvedSearchParams || {}); + const providersData = await getProviders({ page: 1, pageSize: 200 }); + return ( - +
+ + +
-
-
- }> - - -
+
+ }> + + -
- }> - - -
+ }> + + -
- }> - - -
+ }> + + +
-
- - } - > - - -
+
+ + }> + +
); } -const SSRProvidersOverview = async () => { - const providersOverview = await getProvidersOverview({}); - - return ( - <> -

Providers Overview

- - - ); -}; - -const SSRFindingsByStatus = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { - const filters = pickFilterParams(searchParams); - - const findingsByStatus = await getFindingsByStatus({ filters }); - - return ( - <> -

Findings by Status

- - - ); -}; - -const SSRFindingsBySeverity = async ({ - searchParams, -}: { - searchParams: SearchParamsProps | undefined | null; -}) => { - const defaultFilters = { - "filter[status]": "FAIL", - } as const; - - const filters = pickFilterParams(searchParams); - - const combinedFilters = { ...defaultFilters, ...filters }; - - const findingsBySeverity = await getFindingsBySeverity({ - filters: combinedFilters, - }); - - return ( - <> -

- Failed Findings by Severity -

- - - ); -}; - const SSRDataNewFindingsTable = async ({ searchParams, }: { @@ -188,6 +129,7 @@ const SSRDataNewFindingsTable = async ({ return ( <> +

@@ -203,8 +145,6 @@ const SSRDataNewFindingsTable = async ({

- - }> diff --git a/ui/app/(prowler)/roles/page.tsx b/ui/app/(prowler)/roles/page.tsx index 38b4341ac7..e10912576e 100644 --- a/ui/app/(prowler)/roles/page.tsx +++ b/ui/app/(prowler)/roles/page.tsx @@ -1,12 +1,14 @@ import { Spacer } from "@heroui/spacer"; +import Link from "next/link"; import { Suspense } from "react"; import { getRoles } from "@/actions/roles"; import { FilterControls } from "@/components/filters"; import { filterRoles } from "@/components/filters/data-filters"; -import { AddRoleButton } from "@/components/roles"; +import { AddIcon } from "@/components/icons"; import { ColumnsRoles } from "@/components/roles/table"; import { SkeletonTableRoles } from "@/components/roles/table"; +import { Button } from "@/components/shadcn"; import { ContentLayout } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; import { SearchParamsProps } from "@/types"; @@ -22,10 +24,17 @@ export default async function Roles({ return ( - - - - + +
+ + + +
}> diff --git a/ui/app/(prowler)/users/page.tsx b/ui/app/(prowler)/users/page.tsx index 5bf40c7f08..397e8ae94c 100644 --- a/ui/app/(prowler)/users/page.tsx +++ b/ui/app/(prowler)/users/page.tsx @@ -1,13 +1,15 @@ import { Spacer } from "@heroui/spacer"; +import Link from "next/link"; import { Suspense } from "react"; import { getRoles } from "@/actions/roles/roles"; import { getUsers } from "@/actions/users/users"; import { FilterControls } from "@/components/filters"; import { filterUsers } from "@/components/filters/data-filters"; +import { AddIcon } from "@/components/icons"; +import { Button } from "@/components/shadcn"; import { ContentLayout } from "@/components/ui"; import { DataTable, DataTableFilterCustom } from "@/components/ui/table"; -import { AddUserButton } from "@/components/users"; import { ColumnsUser, SkeletonTableUser } from "@/components/users/table"; import { Role, SearchParamsProps, UserProps } from "@/types"; @@ -22,10 +24,17 @@ export default async function Users({ return ( - - - - + +
+ + + +
}> diff --git a/ui/components/ThemeSwitch.tsx b/ui/components/ThemeSwitch.tsx index 24fdebda3d..f9c039db93 100644 --- a/ui/components/ThemeSwitch.tsx +++ b/ui/components/ThemeSwitch.tsx @@ -9,6 +9,12 @@ import { useTheme } from "next-themes"; import { FC } from "react"; import React from "react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; + import { MoonFilledIcon, SunFilledIcon } from "./icons"; export interface ThemeSwitchProps { @@ -41,43 +47,50 @@ export const ThemeSwitch: FC = ({ }); return ( - - - - -
- {!isSelected || isSSR ? ( - - ) : ( - - )} -
-
+ + + + + + +
+ {!isSelected || isSSR ? ( + + ) : ( + + )} +
+
+
+ + {isSelected || isSSR ? "Switch to Dark Mode" : "Switch to Light Mode"} + +
); }; diff --git a/ui/components/auth/oss/auth-footer-link.tsx b/ui/components/auth/oss/auth-footer-link.tsx index bc39e297d8..221a48a400 100644 --- a/ui/components/auth/oss/auth-footer-link.tsx +++ b/ui/components/auth/oss/auth-footer-link.tsx @@ -14,7 +14,7 @@ export const AuthFooterLink = ({ return (

{text}  - + {linkText}

diff --git a/ui/components/auth/oss/sign-in-form.tsx b/ui/components/auth/oss/sign-in-form.tsx index 4c2ce66a93..445ce0938a 100644 --- a/ui/components/auth/oss/sign-in-form.tsx +++ b/ui/components/auth/oss/sign-in-form.tsx @@ -1,6 +1,5 @@ "use client"; -import { Button } from "@heroui/button"; import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; import { useRouter, useSearchParams } from "next/navigation"; @@ -13,8 +12,9 @@ import { AuthDivider } from "@/components/auth/oss/auth-divider"; import { AuthFooterLink } from "@/components/auth/oss/auth-footer-link"; import { AuthLayout } from "@/components/auth/oss/auth-layout"; import { SocialButtons } from "@/components/auth/oss/social-buttons"; +import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { SignInFormData, signInSchema } from "@/types"; @@ -156,32 +156,21 @@ export const SignInForm = ({ type="email" label="Email" placeholder="Enter your email" - isInvalid={!!form.formState.errors.email} showFormMessage /> {!isSamlMode && ( - + )} - - {isLoading ? Loading : Log in} - + {isLoading ? "Loading..." : "Log in"} + @@ -197,21 +186,19 @@ export const SignInForm = ({ /> )}
diff --git a/ui/components/auth/oss/sign-up-form.tsx b/ui/components/auth/oss/sign-up-form.tsx index 917bd2c9e4..723b283196 100644 --- a/ui/components/auth/oss/sign-up-form.tsx +++ b/ui/components/auth/oss/sign-up-form.tsx @@ -11,8 +11,9 @@ import { AuthFooterLink } from "@/components/auth/oss/auth-footer-link"; import { AuthLayout } from "@/components/auth/oss/auth-layout"; import { PasswordRequirementsMessage } from "@/components/auth/oss/password-validator"; import { SocialButtons } from "@/components/auth/oss/social-buttons"; +import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { Form, @@ -133,7 +134,6 @@ export const SignUpForm = ({ type="text" label="Name" placeholder="Enter your name" - isInvalid={!!form.formState.errors.name} /> - + @@ -176,7 +169,6 @@ export const SignUpForm = ({ placeholder={invitationToken} defaultValue={invitationToken} isRequired={false} - isInvalid={!!form.formState.errors.invitationToken} isDisabled={invitationToken !== null && true} /> )} @@ -194,6 +186,7 @@ export const SignUpForm = ({ size="sm" checked={field.value} onChange={(e) => field.onChange(e.target.checked)} + color="default" > I agree with the  - + )} /> )} - - {isLoading ? Loading : Sign up} - + {isLoading ? "Loading..." : "Sign up"} + diff --git a/ui/components/auth/oss/social-buttons.tsx b/ui/components/auth/oss/social-buttons.tsx index 7308a1b4f9..0dd92c48c0 100644 --- a/ui/components/auth/oss/social-buttons.tsx +++ b/ui/components/auth/oss/social-buttons.tsx @@ -1,7 +1,7 @@ -import { Button } from "@heroui/button"; import { Tooltip } from "@heroui/tooltip"; import { Icon } from "@iconify/react"; +import { Button } from "@/components/shadcn"; import { CustomLink } from "@/components/ui/custom/custom-link"; export const SocialButtons = ({ @@ -32,14 +32,22 @@ export const SocialButtons = ({ > @@ -59,16 +67,15 @@ export const SocialButtons = ({ > diff --git a/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx b/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx index 8db5c96ecb..dbf7c74a08 100644 --- a/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx +++ b/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx @@ -58,7 +58,7 @@ export const ClientAccordionWrapper = ({ return (
{!hideExpandButton && ( -
+
- - Go to Scans - -
-
+ +
); diff --git a/ui/components/compliance/threatscore-badge.tsx b/ui/components/compliance/threatscore-badge.tsx index b9ba9d5b7b..542b68d369 100644 --- a/ui/components/compliance/threatscore-badge.tsx +++ b/ui/components/compliance/threatscore-badge.tsx @@ -1,6 +1,5 @@ "use client"; -import { Button } from "@heroui/button"; import { Card, CardBody } from "@heroui/card"; import { Progress } from "@heroui/progress"; import { DownloadIcon, FileTextIcon } from "lucide-react"; @@ -8,6 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { ThreatScoreLogo } from "@/components/compliance/threatscore-logo"; +import { Button } from "@/components/shadcn/button/button"; import { toast } from "@/components/ui"; import { downloadComplianceCsv, downloadThreatScorePdf } from "@/lib/helper"; import type { ScanEntity } from "@/types/scans"; @@ -41,7 +41,7 @@ export const ThreatScoreBadge = ({ const getTextColor = () => { if (score >= 80) return "text-success"; if (score >= 40) return "text-warning"; - return "text-danger"; + return "text-text-error"; }; const handleCardClick = () => { @@ -121,24 +121,25 @@ export const ThreatScoreBadge = ({
diff --git a/ui/components/feeds/feeds-client.tsx b/ui/components/feeds/feeds-client.tsx index fdee89e68b..6e0528b5a1 100644 --- a/ui/components/feeds/feeds-client.tsx +++ b/ui/components/feeds/feeds-client.tsx @@ -14,6 +14,11 @@ import { DropdownMenuTrigger, Separator, } from "@/components/shadcn"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/shadcn/tooltip"; import { hasNewFeeds, markFeedsAsSeen } from "@/lib/feeds-storage"; import { cn } from "@/lib/utils"; @@ -49,30 +54,39 @@ export function FeedsClient({ feedData, error }: FeedsClientProps) { return ( - - - + + + + + + + + {hasUnseenFeeds ? "New updates available" : "Latest Updates"} + + {/* Timeline dot */}
-
+
{!isLast && (
)} @@ -158,13 +172,13 @@ function FeedTimelineItem({ item, isLast }: FeedTimelineItemProps) { className="backdrop-blur-0 block space-y-1 rounded-[12px] border border-transparent p-2 transition-all hover:border-slate-300 hover:bg-[#F8FAFC80] hover:backdrop-blur-[46px] dark:hover:border-[rgba(38,38,38,0.70)] dark:hover:bg-[rgba(23,23,23,0.50)]" >
-

+

{item.title}

{version && ( v{version} @@ -182,7 +196,7 @@ function FeedTimelineItem({ item, isLast }: FeedTimelineItemProps) { {relativeTime} -
+
Read more
diff --git a/ui/components/filters/clear-filters-button.tsx b/ui/components/filters/clear-filters-button.tsx index e30eda2f72..463b2a9af0 100644 --- a/ui/components/filters/clear-filters-button.tsx +++ b/ui/components/filters/clear-filters-button.tsx @@ -1,9 +1,10 @@ "use client"; -import { CrossIcon } from "@/components/icons"; +import { XCircle } from "lucide-react"; + import { useUrlFilters } from "@/hooks/use-url-filters"; -import { CustomButton } from "../ui/custom/custom-button"; +import { Button } from "../shadcn"; export interface ClearFiltersButtonProps { className?: string; @@ -12,7 +13,6 @@ export interface ClearFiltersButtonProps { } export const ClearFiltersButton = ({ - className = "w-full md:w-fit", text = "Clear all filters", ariaLabel = "Reset", }: ClearFiltersButtonProps) => { @@ -23,16 +23,9 @@ export const ClearFiltersButton = ({ } return ( - } - radius="sm" - > + ); }; diff --git a/ui/components/filters/custom-checkbox-muted-findings.tsx b/ui/components/filters/custom-checkbox-muted-findings.tsx index 2ef4460671..ee092a4227 100644 --- a/ui/components/filters/custom-checkbox-muted-findings.tsx +++ b/ui/components/filters/custom-checkbox-muted-findings.tsx @@ -25,7 +25,7 @@ export const CustomCheckboxMutedFindings = () => { }; return ( -
+
{ return (
*]:!rounded-lg", + selectorButton: "text-bg-button-secondary shrink-0", + input: + "text-bg-button-secondary placeholder:text-bg-button-secondary text-sm", + innerWrapper: "[&]:!rounded-lg", + inputWrapper: + "!border-border-input-primary !bg-bg-input-primary dark:!bg-input/30 dark:hover:!bg-input/50 hover:!bg-bg-neutral-secondary !border [&]:!rounded-lg !shadow-xs !transition-[color,box-shadow] focus-within:!border-border-input-primary-press focus-within:!ring-1 focus-within:!ring-border-input-primary-press focus-within:!ring-offset-1 !h-10 !px-4 !py-3 !outline-none", + segment: "text-bg-button-secondary", + }} + popoverProps={{ + classNames: { + content: + "border-border-input-primary bg-bg-input-primary border rounded-lg", + }, + }} CalendarTopContent={ { }} value={value} onChange={handleDateChange} - size="sm" - variant="flat" />
); diff --git a/ui/components/filters/custom-search-input.tsx b/ui/components/filters/custom-search-input.tsx index 99cc66e1dc..f7b1366f4b 100644 --- a/ui/components/filters/custom-search-input.tsx +++ b/ui/components/filters/custom-search-input.tsx @@ -54,16 +54,23 @@ export const CustomSearchInput: React.FC = () => { return ( *]:!rounded-lg", + input: + "text-bg-button-secondary placeholder:text-bg-button-secondary text-sm", + inputWrapper: + "!border-border-input-primary !bg-bg-input-primary dark:!bg-input/30 dark:hover:!bg-input/50 hover:!bg-bg-neutral-secondary !border [&]:!rounded-lg !shadow-xs !transition-[color,box-shadow] focus-within:!border-border-input-primary-press focus-within:!ring-1 focus-within:!ring-border-input-primary-press focus-within:!ring-offset-1 !h-10 !px-4 !py-3 !outline-none", + clearButton: "text-bg-button-secondary", }} aria-label="Search" - label="Search" placeholder="Search..." - labelPlacement="inside" value={searchQuery} - startContent={} + startContent={ + + } onChange={(e) => { const value = e.target.value; setSearchQuery(value); @@ -71,13 +78,14 @@ export const CustomSearchInput: React.FC = () => { }} endContent={ searchQuery && ( - ) } - radius="sm" - size="sm" /> ); }; diff --git a/ui/components/filters/filter-controls.tsx b/ui/components/filters/filter-controls.tsx index 34392ad2ec..fa149fbcec 100644 --- a/ui/components/filters/filter-controls.tsx +++ b/ui/components/filters/filter-controls.tsx @@ -1,13 +1,11 @@ "use client"; import { Spacer } from "@heroui/spacer"; -import { useSearchParams } from "next/navigation"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { FilterOption } from "@/types"; import { DataTableFilterCustom } from "../ui/table"; -import { ClearFiltersButton } from "./clear-filters-button"; import { CustomAccountSelection } from "./custom-account-selection"; import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings"; import { CustomDatePicker } from "./custom-date-picker"; @@ -23,7 +21,6 @@ export interface FilterControlsProps { accounts?: boolean; mutedFindings?: boolean; customFilters?: FilterOption[]; - showClearButton?: boolean; } export const FilterControls: React.FC = ({ @@ -33,40 +30,22 @@ export const FilterControls: React.FC = ({ regions = false, accounts = false, mutedFindings = false, - showClearButton = true, customFilters, }) => { - const searchParams = useSearchParams(); - const [hasFilters, setHasFilters] = useState(false); - - useEffect(() => { - const hasFilters = Array.from(searchParams.keys()).some( - (key) => key.startsWith("filter[") || key === "sort", - ); - setHasFilters(hasFilters); - }, [searchParams]); - return (
-
- {search && } - {providers && } - {date && } - {regions && } - {accounts && } - {mutedFindings && } - {!customFilters && hasFilters && showClearButton && ( - - )} +
+
+ {search && } + {providers && } + {date && } + {regions && } + {accounts && } + {mutedFindings && } +
- {customFilters && ( - - )} + {customFilters && }
); }; diff --git a/ui/components/findings/send-to-jira-modal.tsx b/ui/components/findings/send-to-jira-modal.tsx index 2521ac6f36..4a3bdc862c 100644 --- a/ui/components/findings/send-to-jira-modal.tsx +++ b/ui/components/findings/send-to-jira-modal.tsx @@ -282,7 +282,7 @@ export const SendToJiraModal = ({ ))} - + )} /> @@ -366,105 +366,38 @@ export const SendToJiraModal = ({ ))} - + )} /> )} - {/* Issue Type Selection - Enhanced Style */} - {/* {selectedProject && issueTypes.length > 0 && ( - ( - <> - - } - value={searchIssueTypeValue} - onValueChange={setSearchIssueTypeValue} - onClear={() => setSearchIssueTypeValue("")} - classNames={{ - inputWrapper: - "border-default-200 bg-transparent hover:bg-default-100/50", - input: "text-small", - }} - /> -
- ) : null, - }} - > - {filteredIssueTypes.map((type) => ( - -
- {type} -
-
- ))} - - - - - )} - /> - )} */} - {/* No integrations or none connected message */} {!isFetchingIntegrations && - (integrations.length === 0 || !hasConnectedIntegration) && ( - - )} - - onOpenChange(false)} - submitText="Send to Jira" - cancelText="Cancel" - loadingText="Sending..." - isDisabled={ - !form.formState.isValid || - form.formState.isSubmitting || - isFetchingIntegrations || - integrations.length === 0 || - !hasConnectedIntegration - } - rightIcon={} - /> + (integrations.length === 0 || !hasConnectedIntegration) ? ( + + ) : ( + onOpenChange(false)} + submitText="Send to Jira" + cancelText="Cancel" + loadingText="Sending..." + isDisabled={ + !form.formState.isValid || + form.formState.isSubmitting || + isFetchingIntegrations || + integrations.length === 0 || + !hasConnectedIntegration + } + rightIcon={} + /> + )} diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index 7d27c400f4..751f33673d 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -71,7 +71,9 @@ const FindingDetailsCell = ({ row }: { row: any }) => { return (
} + triggerComponent={ + + } title="Finding Details" description="View the finding details" defaultOpen={isOpen} diff --git a/ui/components/findings/table/data-table-row-actions.tsx b/ui/components/findings/table/data-table-row-actions.tsx index 12945326d3..e3ec163cf8 100644 --- a/ui/components/findings/table/data-table-row-actions.tsx +++ b/ui/components/findings/table/data-table-row-actions.tsx @@ -1,6 +1,5 @@ "use client"; -import { Button } from "@heroui/button"; import { Dropdown, DropdownItem, @@ -14,6 +13,7 @@ import { useState } from "react"; import { SendToJiraModal } from "@/components/findings/send-to-jira-modal"; import { VerticalDotsIcon } from "@/components/icons"; import { JiraIcon } from "@/components/icons/services/IconServices"; +import { Button } from "@/components/shadcn"; import type { FindingProps } from "@/types/components"; interface DataTableRowActionsProps { @@ -38,12 +38,12 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
- { ? "New finding." : "Status changed since the previous scan."} - - Learn more - + + Learn more + +
} > diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index 61f7408f3d..d3a9f38e98 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -5,8 +5,14 @@ import { Tooltip } from "@heroui/tooltip"; import { ExternalLink, Link } from "lucide-react"; import ReactMarkdown from "react-markdown"; +import { + Card, + CardAction, + CardContent, + CardHeader, + CardTitle, +} from "@/components/shadcn"; import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; -import { CustomSection } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { EntityInfoShort, InfoField } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; @@ -92,7 +98,13 @@ export const FindingDetail = ({ isMuted={attributes.muted} mutedReason={attributes.muted_reason || ""} /> +
+
+ {/* Check Metadata */} + + + Finding Details
{renderValue(attributes.status)}
-
-
- - {/* Check Metadata */} - -
- - - {attributes.check_metadata.servicename} - - {resource.region} - - - - {attributes.delta && ( - -
- - {attributes.delta} -
+ + +
+ + + {attributes.check_metadata.servicename} - )} - - - -
- - - - - - - - - - - - - - {attributes.status === "FAIL" && ( - - - - {attributes.check_metadata.risk} - - - - )} - - - - {attributes.check_metadata.description} - - - - - {renderValue(attributes.status_extended)} - - - {attributes.check_metadata.remediation && ( -
-

- Remediation Details -

- - {/* Recommendation section */} - {attributes.check_metadata.remediation.recommendation.text && ( - -
- - {attributes.check_metadata.remediation.recommendation.text} - - - {attributes.check_metadata.remediation.recommendation.url && ( - - Learn more - - )} + {resource.region} + + + + {attributes.delta && ( + +
+ + {attributes.delta}
)} + + + +
+ + + + + + + + + + + + - {/* CLI Command section */} - {attributes.check_metadata.remediation.code.cli && ( - - - - {attributes.check_metadata.remediation.code.cli} - - - - )} - - {/* Remediation Steps section */} - {attributes.check_metadata.remediation.code.other && ( - + {attributes.status === "FAIL" && ( + + - {attributes.check_metadata.remediation.code.other} + {attributes.check_metadata.risk} - - )} - - {/* Additional URLs section */} - {attributes.check_metadata.additionalurls && - attributes.check_metadata.additionalurls.length > 0 && ( - -
    - {attributes.check_metadata.additionalurls.map( - (link, idx) => ( -
  • - - {link} - -
  • - ), - )} -
-
- )} -
- )} - - - {attributes.check_metadata.categories?.join(", ") || "-"} - - - - {/* Resource Details */} - - Resource Details - {gitUrl && ( - - - - - - )} - - ) : ( - "Resource Details" - ) - } - > -
- - {renderValue(resource.name)} - - - {renderValue(resource.type)} - -
- -
- {renderValue(resource.service)} - {renderValue(resource.region)} -
- - {resource.tags && Object.entries(resource.tags).length > 0 && ( -
-

- Tags -

-
- {Object.entries(resource.tags).map(([key, value]) => ( - - {renderValue(value)} - - ))} -
-
- )} - -
- - - - - - -
-
- - {/* Add new Scan Details section */} - -
- {scan.name || "N/A"} - - {scan.unique_resource_count} - - {scan.progress}% -
- -
- {scan.trigger} - {scan.state} - - {formatDuration(scan.duration)} - -
- -
- - - - - - -
- -
- - - - {scan.scheduled_at && ( - - + )} -
-
+ + + + {attributes.check_metadata.description} + + + + + {renderValue(attributes.status_extended)} + + + {attributes.check_metadata.remediation && ( +
+

+ Remediation Details +

+ + {/* Recommendation section */} + {attributes.check_metadata.remediation.recommendation.text && ( + +
+ + { + attributes.check_metadata.remediation.recommendation + .text + } + + + {attributes.check_metadata.remediation.recommendation + .url && ( + + Learn more + + )} +
+
+ )} + + {/* CLI Command section */} + {attributes.check_metadata.remediation.code.cli && ( + + + + {attributes.check_metadata.remediation.code.cli} + + + + )} + + {/* Remediation Steps section */} + {attributes.check_metadata.remediation.code.other && ( + + + {attributes.check_metadata.remediation.code.other} + + + )} + + {/* Additional URLs section */} + {attributes.check_metadata.additionalurls && + attributes.check_metadata.additionalurls.length > 0 && ( + +
    + {attributes.check_metadata.additionalurls.map( + (link, idx) => ( +
  • + + {link} + +
  • + ), + )} +
+
+ )} +
+ )} + + + {attributes.check_metadata.categories?.join(", ") || "-"} + +
+ + + {/* Resource Details */} + + + Resource Details + {providerDetails.provider === "iac" && gitUrl && ( + + + + + + + + )} + + +
+ + {renderValue(resource.name)} + + + {renderValue(resource.type)} + +
+ +
+ + {renderValue(resource.service)} + + {renderValue(resource.region)} +
+ + {resource.tags && Object.entries(resource.tags).length > 0 && ( +
+

+ Tags +

+
+ {Object.entries(resource.tags).map(([key, value]) => ( + + {renderValue(value)} + + ))} +
+
+ )} + +
+ + + + + + +
+
+
+ + {/* Add new Scan Details section */} + + + Scan Details + + +
+ {scan.name || "N/A"} + + {scan.unique_resource_count} + + {scan.progress}% +
+ +
+ {scan.trigger} + {scan.state} + + {formatDuration(scan.duration)} + +
+ +
+ + + + + + +
+ +
+ + + + {scan.scheduled_at && ( + + + + )} +
+
+
); }; diff --git a/ui/components/findings/table/skeleton-table-findings.tsx b/ui/components/findings/table/skeleton-table-findings.tsx index 865fd14911..4436f22f60 100644 --- a/ui/components/findings/table/skeleton-table-findings.tsx +++ b/ui/components/findings/table/skeleton-table-findings.tsx @@ -1,11 +1,39 @@ import React from "react"; -import { SkeletonTable } from "../../ui/skeleton/skeleton"; +import { Card } from "@/components/shadcn/card/card"; +import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; export const SkeletonTableFindings = () => { + const columns = 7; + const rows = 4; + 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/graphs/horizontal-bar-chart.tsx b/ui/components/graphs/horizontal-bar-chart.tsx index f36f20668b..9b1291fa60 100644 --- a/ui/components/graphs/horizontal-bar-chart.tsx +++ b/ui/components/graphs/horizontal-bar-chart.tsx @@ -11,9 +11,14 @@ interface HorizontalBarChartProps { data: BarDataPoint[]; height?: number; title?: string; + labelWidth?: string; } -export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) { +export function HorizontalBarChart({ + data, + title, + labelWidth = "w-20", +}: HorizontalBarChartProps) { const [hoveredIndex, setHoveredIndex] = useState(null); const total = data.reduce((sum, d) => sum + (Number(d.value) || 0), 0); @@ -61,13 +66,14 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) { onMouseLeave={() => !isEmpty && setHoveredIndex(null)} > {/* Label */} -
+
{item.name === "Informational" ? "Info" : item.name} @@ -134,17 +140,17 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) { {/* Percentage and Count */}
- + {isEmpty ? "0" : item.percentage}% - - + + {isEmpty ? "0" : item.value.toLocaleString()}
diff --git a/ui/components/graphs/shared/alert-pill.tsx b/ui/components/graphs/shared/alert-pill.tsx index 46997f4031..4dcae9e51e 100644 --- a/ui/components/graphs/shared/alert-pill.tsx +++ b/ui/components/graphs/shared/alert-pill.tsx @@ -32,11 +32,11 @@ export function AlertPill({ > {value} diff --git a/ui/components/graphs/threat-map.tsx b/ui/components/graphs/threat-map.tsx index f2e79768a3..8435d17c3a 100644 --- a/ui/components/graphs/threat-map.tsx +++ b/ui/components/graphs/threat-map.tsx @@ -70,10 +70,10 @@ function getMapColors(): MapColorsConfig { landStroke: getVar("--border-neutral-tertiary") || DEFAULT_MAP_COLORS.landStroke, pointDefault: - getVar("--text-error-primary") || DEFAULT_MAP_COLORS.pointDefault, + getVar("--text-text-error") || DEFAULT_MAP_COLORS.pointDefault, pointSelected: getVar("--bg-button-primary") || DEFAULT_MAP_COLORS.pointSelected, - pointHover: getVar("--text-error-primary") || DEFAULT_MAP_COLORS.pointHover, + pointHover: getVar("--text-text-error") || DEFAULT_MAP_COLORS.pointHover, }; return colors; diff --git a/ui/components/icons/Icons.tsx b/ui/components/icons/Icons.tsx index 3d0275af2f..ba353879ac 100644 --- a/ui/components/icons/Icons.tsx +++ b/ui/components/icons/Icons.tsx @@ -1131,7 +1131,7 @@ export const LighthouseIcon: React.FC = ({ width={size || width} height={size || height} fill="none" - stroke="#8ce112" + stroke="var(--bg-button-primary)" strokeWidth="12" strokeLinecap="round" strokeLinejoin="round" @@ -1188,3 +1188,69 @@ export const BellIcon: React.FC = ({ ); }; + +export const SidebarExpandIcon: React.FC = ({ + size = 24, + width, + height, + ...props +}) => { + return ( + + + + + ); +}; + +export const SidebarCollapseIcon: React.FC = ({ + size = 24, + width, + height, + ...props +}) => { + return ( + + + + + ); +}; diff --git a/ui/components/integrations/jira/jira-integration-card.tsx b/ui/components/integrations/jira/jira-integration-card.tsx index 23fd901a9f..1aa391e4b4 100644 --- a/ui/components/integrations/jira/jira-integration-card.tsx +++ b/ui/components/integrations/jira/jira-integration-card.tsx @@ -1,16 +1,18 @@ "use client"; -import { Card, CardBody, CardHeader } from "@heroui/card"; import { SettingsIcon } from "lucide-react"; +import Link from "next/link"; import { JiraIcon } from "@/components/icons/services/IconServices"; -import { CustomButton } from "@/components/ui/custom"; +import { Button } from "@/components/shadcn"; import { CustomLink } from "@/components/ui/custom/custom-link"; +import { Card, CardContent, CardHeader } from "../../shadcn"; + export const JiraIntegrationCard = () => { return ( - - + +
@@ -33,26 +35,21 @@ export const JiraIntegrationCard = () => {
- } - asLink="/integrations/jira" - ariaLabel="Manage Jira integrations" - > - Manage - +
- -
-

- Configure and manage your Jira integrations to automatically create - issues for security findings in your Jira projects. -

-
-
+ +

+ Configure and manage your Jira integrations to automatically create + issues for security findings in your Jira projects. +

+
); }; diff --git a/ui/components/integrations/jira/jira-integration-form.tsx b/ui/components/integrations/jira/jira-integration-form.tsx index f90ddf70af..90e00cbc6b 100644 --- a/ui/components/integrations/jira/jira-integration-form.tsx +++ b/ui/components/integrations/jira/jira-integration-form.tsx @@ -152,7 +152,6 @@ export const JiraIntegrationForm = ({ placeholder="your-domain.atlassian.net" isRequired isDisabled={isLoading} - isInvalid={!!form.formState.errors.domain} /> )} @@ -165,7 +164,6 @@ export const JiraIntegrationForm = ({ labelPlacement="inside" placeholder="your-domain.atlassian.net" isDisabled={isLoading} - isInvalid={!!form.formState.errors.domain} /> )} @@ -178,7 +176,6 @@ export const JiraIntegrationForm = ({ placeholder="user@example.com" isRequired isDisabled={isLoading} - isInvalid={!!form.formState.errors.user_mail} />
diff --git a/ui/components/integrations/jira/jira-integrations-manager.tsx b/ui/components/integrations/jira/jira-integrations-manager.tsx index ab57a8ef0a..0ad5f58f9a 100644 --- a/ui/components/integrations/jira/jira-integrations-manager.tsx +++ b/ui/components/integrations/jira/jira-integrations-manager.tsx @@ -1,6 +1,5 @@ "use client"; -import { Card, CardBody, CardHeader } from "@heroui/card"; import { format } from "date-fns"; import { PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; @@ -16,13 +15,15 @@ import { IntegrationCardHeader, IntegrationSkeleton, } from "@/components/integrations/shared"; +import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { CustomAlertModal } from "@/components/ui/custom"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper"; import { MetaDataProps } from "@/types"; import { IntegrationProps } from "@/types/integrations"; +import { Card, CardContent, CardHeader } from "../../shadcn"; import { JiraIntegrationForm } from "./jira-integration-form"; interface JiraIntegrationsManagerProps { @@ -214,38 +215,33 @@ export const JiraIntegrationsManager = ({ title="Delete Jira Integration" description="This action cannot be undone. This will permanently delete your Jira integration." > -
- + - } - onPress={() => + disabled={isDeleting !== null} + onClick={() => integrationToDelete && handleDeleteIntegration(integrationToDelete.id) } > + {!isDeleting && } {isDeleting ? "Deleting..." : "Delete"} - +
@@ -278,14 +274,10 @@ export const JiraIntegrationsManager = ({ : `${integrations.length} integration${integrations.length !== 1 ? "s" : ""} configured`}

- } - onPress={handleAddIntegration} - ariaLabel="Add integration" - > +
{/* Integrations List */} @@ -300,8 +292,8 @@ export const JiraIntegrationsManager = ({ ) : integrations.length > 0 ? (
{integrations.map((integration) => ( - - + + } title={`${integration.attributes.configuration.domain}`} @@ -311,7 +303,7 @@ export const JiraIntegrationsManager = ({ /> - +
{integration.attributes.connection_last_checked_at && ( @@ -335,7 +327,7 @@ export const JiraIntegrationsManager = ({ isTesting={isTesting === integration.id} />
- + ))}
diff --git a/ui/components/integrations/s3/s3-integration-card.tsx b/ui/components/integrations/s3/s3-integration-card.tsx index 5b3e309b0a..e777a729de 100644 --- a/ui/components/integrations/s3/s3-integration-card.tsx +++ b/ui/components/integrations/s3/s3-integration-card.tsx @@ -1,16 +1,18 @@ "use client"; -import { Card, CardBody, CardHeader } from "@heroui/card"; import { SettingsIcon } from "lucide-react"; +import Link from "next/link"; import { AmazonS3Icon } from "@/components/icons/services/IconServices"; -import { CustomButton } from "@/components/ui/custom"; +import { Button } from "@/components/shadcn"; import { CustomLink } from "@/components/ui/custom/custom-link"; +import { Card, CardContent, CardHeader } from "../../shadcn"; + export const S3IntegrationCard = () => { return ( - - + +
@@ -33,26 +35,21 @@ export const S3IntegrationCard = () => {
- } - asLink="/integrations/amazon-s3" - ariaLabel="Manage S3 integrations" - > - Manage - +
- -
-

- Configure and manage your Amazon S3 integrations to automatically - export security findings to your S3 buckets. -

-
-
+ +

+ Configure and manage your Amazon S3 integrations to automatically + export security findings to your S3 buckets. +

+
); }; diff --git a/ui/components/integrations/s3/s3-integration-form.tsx b/ui/components/integrations/s3/s3-integration-form.tsx index 4d78ba732d..2570441b29 100644 --- a/ui/components/integrations/s3/s3-integration-form.tsx +++ b/ui/components/integrations/s3/s3-integration-form.tsx @@ -282,7 +282,6 @@ export const S3IntegrationForm = ({ providers={providers} label="Cloud Providers" placeholder="Select providers to integrate with" - isInvalid={!!form.formState.errors.providers} selectionMode="multiple" enableSearch={true} /> @@ -301,7 +300,6 @@ export const S3IntegrationForm = ({ placeholder="my-security-findings-bucket" variant="bordered" isRequired - isInvalid={!!form.formState.errors.bucket_name} />
diff --git a/ui/components/integrations/s3/s3-integrations-manager.tsx b/ui/components/integrations/s3/s3-integrations-manager.tsx index 0884e7d1b4..6246f941fe 100644 --- a/ui/components/integrations/s3/s3-integrations-manager.tsx +++ b/ui/components/integrations/s3/s3-integrations-manager.tsx @@ -1,6 +1,5 @@ "use client"; -import { Card, CardBody, CardHeader } from "@heroui/card"; import { format } from "date-fns"; import { PlusIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; @@ -16,14 +15,16 @@ import { IntegrationCardHeader, IntegrationSkeleton, } from "@/components/integrations/shared"; +import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { CustomAlertModal } from "@/components/ui/custom"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper"; import { MetaDataProps } from "@/types"; import { IntegrationProps } from "@/types/integrations"; import { ProviderProps } from "@/types/providers"; +import { Card, CardContent, CardHeader } from "../../shadcn"; import { S3IntegrationForm } from "./s3-integration-form"; interface S3IntegrationsManagerProps { @@ -214,38 +215,33 @@ export const S3IntegrationsManager = ({ title="Delete S3 Integration" description="This action cannot be undone. This will permanently delete your S3 integration." > -
- + - } - onPress={() => + disabled={isDeleting !== null} + onClick={() => integrationToDelete && handleDeleteIntegration(integrationToDelete.id) } > + {!isDeleting && } {isDeleting ? "Deleting..." : "Delete"} - +
@@ -284,14 +280,10 @@ export const S3IntegrationsManager = ({ : `${integrations.length} integration${integrations.length !== 1 ? "s" : ""} configured`}

- } - onPress={handleAddIntegration} - ariaLabel="Add integration" - > +
{/* Integrations List */} @@ -306,8 +298,8 @@ export const S3IntegrationsManager = ({ ) : integrations.length > 0 ? (
{integrations.map((integration) => ( - - + + } title={ @@ -326,7 +318,7 @@ export const S3IntegrationsManager = ({ /> - +
{integration.attributes.connection_last_checked_at && ( @@ -351,7 +343,7 @@ export const S3IntegrationsManager = ({ isTesting={isTesting === integration.id} />
- + ))}
diff --git a/ui/components/integrations/saml/saml-config-form.tsx b/ui/components/integrations/saml/saml-config-form.tsx index 397be82702..d52e4e4ba7 100644 --- a/ui/components/integrations/saml/saml-config-form.tsx +++ b/ui/components/integrations/saml/saml-config-form.tsx @@ -12,8 +12,9 @@ import { z } from "zod"; import { createSamlConfig, updateSamlConfig } from "@/actions/integrations"; import { AddIcon } from "@/components/icons"; +import { Button, Card, CardContent, CardHeader } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomButton, CustomServerInput } from "@/components/ui/custom"; +import { CustomServerInput } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; import { SnippetChip } from "@/components/ui/entities"; import { FormButtons } from "@/components/ui/form"; @@ -293,75 +294,76 @@ export const SamlConfigForm = ({ }} /> -
-

+ + Identity Provider Configuration -

+ + +
+
+ + ACS URL: + + +
-
-
- - ACS URL: - - -
+
+ + Audience: + + +
-
- - Audience: - - -
+
+ + Name ID Format: + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + +
-
- - Name ID Format: - - - urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress - +
+ + Supported Assertion Attributes: + +
    +
  • • firstName
  • +
  • • lastName
  • +
  • • userType
  • +
  • • organization
  • +
+

+ Note: The userType attribute will be used to + assign the user's role. If the role does not exist, one + will be created with minimal permissions. You can assign + permissions to roles on the{" "} + + Roles + {" "} + page. +

+
- -
- - Supported Assertion Attributes: - -
    -
  • • firstName
  • -
  • • lastName
  • -
  • • userType
  • -
  • • organization
  • -
-

- Note: The userType attribute will be used to - assign the user's role. If the role does not exist, one will - be created with minimal permissions. You can assign permissions to - roles on the{" "} - - Roles - {" "} - page. -

-
-
-
+
+
- + Metadata XML File * - { + variant="outline" + disabled={isPending} + onClick={() => { const fileInput = document.getElementById( "metadata_xml_file", ) as HTMLInputElement; @@ -369,8 +371,7 @@ export const SamlConfigForm = ({ fileInput.click(); } }} - startContent={} - className={`rounded-medium text-default-500 h-10 justify-start border-2 ${ + className={`justify-start gap-2 ${ ( clientErrors.metadata_xml === null ? undefined @@ -379,10 +380,11 @@ export const SamlConfigForm = ({ ? "border-red-500" : uploadedFile ? "border-green-500 bg-green-50 dark:bg-green-900/20" - : "border-default-200" + : "" }`} > - + + {uploadedFile ? ( {uploadedFile.name} @@ -391,7 +393,7 @@ export const SamlConfigForm = ({ "Choose File" )} - + { @@ -60,12 +61,12 @@ export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => { /> - - + +

SAML SSO Integration

- {id && } + {id && }

{id ? ( @@ -81,39 +82,32 @@ export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => {

- +
Status: - + {id ? "Enabled" : "Disabled"}
- setIsSamlModalOpen(true)} - > + {id && ( - : null} - onPress={handleRemoveSaml} + variant="destructive" + disabled={isDeleting} + onClick={handleRemoveSaml} > - Remove - + {!isDeleting && } + {isDeleting ? "Removing..." : "Remove"} + )}
-
+
); diff --git a/ui/components/integrations/security-hub/security-hub-integration-card.tsx b/ui/components/integrations/security-hub/security-hub-integration-card.tsx index 6a3cd48725..bce91812f4 100644 --- a/ui/components/integrations/security-hub/security-hub-integration-card.tsx +++ b/ui/components/integrations/security-hub/security-hub-integration-card.tsx @@ -1,16 +1,18 @@ "use client"; -import { Card, CardBody, CardHeader } from "@heroui/card"; import { SettingsIcon } from "lucide-react"; +import Link from "next/link"; import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices"; -import { CustomButton } from "@/components/ui/custom"; +import { Button } from "@/components/shadcn"; import { CustomLink } from "@/components/ui/custom/custom-link"; +import { Card, CardContent, CardHeader } from "../../shadcn"; + export const SecurityHubIntegrationCard = () => { return ( - - + +
@@ -33,26 +35,21 @@ export const SecurityHubIntegrationCard = () => {
- } - asLink="/integrations/aws-security-hub" - ariaLabel="Manage Security Hub integrations" - > - Manage - +
- -
-

- Configure and manage your AWS Security Hub integrations to - automatically send security findings for centralized monitoring. -

-
-
+ +

+ Configure and manage your AWS Security Hub integrations to + automatically send security findings for centralized monitoring. +

+
); }; diff --git a/ui/components/integrations/security-hub/security-hub-integration-form.tsx b/ui/components/integrations/security-hub/security-hub-integration-form.tsx index 0e33be2ca3..1acfa31809 100644 --- a/ui/components/integrations/security-hub/security-hub-integration-form.tsx +++ b/ui/components/integrations/security-hub/security-hub-integration-form.tsx @@ -352,6 +352,7 @@ export const SecurityHubIntegrationForm = ({ isSelected={Boolean(field.value)} onValueChange={field.onChange} size="sm" + color="default" > Send only findings with status FAIL @@ -370,6 +371,7 @@ export const SecurityHubIntegrationForm = ({ isSelected={Boolean(field.value)} onValueChange={field.onChange} size="sm" + color="default" > Archive previous findings @@ -387,6 +389,7 @@ export const SecurityHubIntegrationForm = ({ isSelected={field.value} onValueChange={field.onChange} size="sm" + color="default" > Use custom credentials (By default, AWS account ones diff --git a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx index 612b82cfc7..270318008a 100644 --- a/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx +++ b/ui/components/integrations/security-hub/security-hub-integrations-manager.tsx @@ -1,6 +1,5 @@ "use client"; -import { Card, CardBody, CardHeader } from "@heroui/card"; import { Chip } from "@heroui/chip"; import { format } from "date-fns"; import { PlusIcon, Trash2Icon } from "lucide-react"; @@ -17,14 +16,16 @@ import { IntegrationCardHeader, IntegrationSkeleton, } from "@/components/integrations/shared"; +import { Button } from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { CustomAlertModal } from "@/components/ui/custom"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper"; import { MetaDataProps } from "@/types"; import { IntegrationProps } from "@/types/integrations"; import { ProviderProps } from "@/types/providers"; +import { Card, CardContent, CardHeader } from "../../shadcn"; import { SecurityHubIntegrationForm } from "./security-hub-integration-form"; interface SecurityHubIntegrationsManagerProps { @@ -264,38 +265,33 @@ export const SecurityHubIntegrationsManager = ({ title="Delete Security Hub Integration" description="This action cannot be undone. This will permanently delete your Security Hub integration." > -
- + - } - onPress={() => + disabled={isDeleting !== null} + onClick={() => integrationToDelete && handleDeleteIntegration(integrationToDelete.id) } > + {!isDeleting && } {isDeleting ? "Deleting..." : "Delete"} - +
@@ -334,14 +330,10 @@ export const SecurityHubIntegrationsManager = ({ : `${integrations.length} integration${integrations.length !== 1 ? "s" : ""} configured`}

- } - onPress={handleAddIntegration} - ariaLabel="Add integration" - > +
{isOperationLoading ? ( @@ -359,8 +351,8 @@ export const SecurityHubIntegrationsManager = ({ const providerDetails = getProviderDetails(integration); return ( - - + + } title={providerDetails.displayName} @@ -388,7 +380,7 @@ export const SecurityHubIntegrationsManager = ({ }} /> - +
{enabledRegions.length > 0 && (
@@ -397,7 +389,7 @@ export const SecurityHubIntegrationsManager = ({ key={region} size="sm" variant="flat" - className="bg-default-100" + className="bg-bg-neutral-secondary" > {region} @@ -428,7 +420,7 @@ export const SecurityHubIntegrationsManager = ({ />
-
+
); })} diff --git a/ui/components/integrations/shared/integration-action-buttons.tsx b/ui/components/integrations/shared/integration-action-buttons.tsx index cc771135ba..983c38ab64 100644 --- a/ui/components/integrations/shared/integration-action-buttons.tsx +++ b/ui/components/integrations/shared/integration-action-buttons.tsx @@ -8,7 +8,7 @@ import { Trash2Icon, } from "lucide-react"; -import { CustomButton } from "@/components/ui/custom"; +import { Button } from "@/components/shadcn"; import { IntegrationProps } from "@/types/integrations"; interface IntegrationActionButtonsProps { @@ -34,69 +34,57 @@ export const IntegrationActionButtons = ({ }: IntegrationActionButtonsProps) => { return (
- } - onPress={() => onTestConnection(integration.id)} - isLoading={isTesting} - isDisabled={!integration.attributes.enabled || isTesting} - ariaLabel="Test connection" + variant="outline" + onClick={() => onTestConnection(integration.id)} + disabled={!integration.attributes.enabled || isTesting} className="w-full sm:w-auto" > - Test - + + {isTesting ? "Testing..." : "Test"} + {onEditConfiguration && ( - } - onPress={() => onEditConfiguration(integration)} - ariaLabel="Edit configuration" + variant="outline" + onClick={() => onEditConfiguration(integration)} className="w-full sm:w-auto" > + Config - + )} {showCredentialsButton && ( - } - onPress={() => onEditCredentials(integration)} - ariaLabel="Edit credentials" + variant="outline" + onClick={() => onEditCredentials(integration)} className="w-full sm:w-auto" > + Credentials - + )} - } - onPress={() => onToggleEnabled(integration)} - isDisabled={isTesting} - ariaLabel={ - integration.attributes.enabled - ? "Disable integration" - : "Enable integration" - } + variant="outline" + onClick={() => onToggleEnabled(integration)} + disabled={isTesting} className="w-full sm:w-auto" > + {integration.attributes.enabled ? "Disable" : "Enable"} - - +
); }; diff --git a/ui/components/invitations/forms/delete-form.tsx b/ui/components/invitations/forms/delete-form.tsx index 785d63b51f..097ad9620d 100644 --- a/ui/components/invitations/forms/delete-form.tsx +++ b/ui/components/invitations/forms/delete-form.tsx @@ -7,8 +7,8 @@ import * as z from "zod"; import { revokeInvite } from "@/actions/invitations/invitation"; 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({ @@ -62,33 +62,26 @@ export const DeleteForm = ({
-
- + - } + disabled={isLoading} > - {isLoading ? <>Loading : Revoke} - + {!isLoading && } + {isLoading ? "Loading" : "Revoke"} +
diff --git a/ui/components/invitations/forms/edit-form.tsx b/ui/components/invitations/forms/edit-form.tsx index 507dff285f..d087f34729 100644 --- a/ui/components/invitations/forms/edit-form.tsx +++ b/ui/components/invitations/forms/edit-form.tsx @@ -6,12 +6,13 @@ import { Controller, useForm } from "react-hook-form"; import * as z from "zod"; import { updateInvite } from "@/actions/invitations/invitation"; -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 { editInviteFormSchema } from "@/types"; +import { Card, CardContent } from "../../shadcn"; + export const EditForm = ({ invitationId, invitationEmail, @@ -94,22 +95,24 @@ export const EditForm = ({ onSubmit={form.handleSubmit(onSubmitClient)} className="flex flex-col gap-4" > -
-
- - Email: - - {invitationEmail} - -
-
- - Role: - - {currentRole} - -
-
+ + +
+ + Email: + + {invitationEmail} + +
+
+ + Role: + + {currentRole} + +
+
+
@@ -157,33 +159,7 @@ export const EditForm = ({
-
- setIsOpen(false)} - isDisabled={isLoading} - > - Cancel - - - } - > - {isLoading ? <>Loading : Save} - -
+ ); diff --git a/ui/components/invitations/index.ts b/ui/components/invitations/index.ts index e381a114df..420b55657e 100644 --- a/ui/components/invitations/index.ts +++ b/ui/components/invitations/index.ts @@ -1,2 +1 @@ export * from "./invitation-details"; -export * from "./send-invitation-button"; diff --git a/ui/components/invitations/invitation-details.tsx b/ui/components/invitations/invitation-details.tsx index a7bba418db..c7b0c7b23f 100644 --- a/ui/components/invitations/invitation-details.tsx +++ b/ui/components/invitations/invitation-details.tsx @@ -1,11 +1,11 @@ "use client"; -import { Card, CardBody } from "@heroui/card"; -import { Divider } from "@heroui/divider"; import { Snippet } from "@heroui/snippet"; +import Link from "next/link"; import { AddIcon } from "../icons"; -import { CustomButton } from "../ui/custom"; +import { Button, Card, CardContent, CardHeader } from "../shadcn"; +import { Separator } from "../shadcn/separator/separator"; import { DateWithTime } from "../ui/entities"; interface InvitationDetailsProps { @@ -35,9 +35,11 @@ const InfoField = ({ children: React.ReactNode; }) => (
- {label} -
- {children} + + {label} + +
+ {children}
); @@ -53,17 +55,9 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => { return (
- - -

- Invitation details -

- - + + Invitation details +
{attributes.email} @@ -88,8 +82,8 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => {
- -

+ +

Share this link with the user:

@@ -100,26 +94,22 @@ export const InvitationDetails = ({ attributes }: InvitationDetailsProps) => { }} hideSymbol variant="bordered" - className="overflow-hidden bg-gray-50 py-1 text-ellipsis whitespace-nowrap dark:bg-slate-800" + className="bg-bg-neutral-secondary overflow-hidden py-1 text-ellipsis whitespace-nowrap" > -

+

{invitationLink}

- +
- } - > - Back to Invitations - +
); diff --git a/ui/components/invitations/send-invitation-button.tsx b/ui/components/invitations/send-invitation-button.tsx deleted file mode 100644 index ae13d33b1d..0000000000 --- a/ui/components/invitations/send-invitation-button.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { AddIcon } from "../icons"; -import { CustomButton } from "../ui/custom"; - -export const SendInvitationButton = () => { - return ( -
- } - > - Send Invitation - -
- ); -}; diff --git a/ui/components/invitations/table/data-table-row-actions.tsx b/ui/components/invitations/table/data-table-row-actions.tsx index 79cb793f65..320f84fc35 100644 --- a/ui/components/invitations/table/data-table-row-actions.tsx +++ b/ui/components/invitations/table/data-table-row-actions.tsx @@ -1,6 +1,5 @@ "use client"; -import { Button } from "@heroui/button"; import { Dropdown, DropdownItem, @@ -19,6 +18,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 { DeleteForm, EditForm } from "../forms"; @@ -68,12 +68,12 @@ export function DataTableRowActions({
- ({ } onPress={() => setIsDeleteOpen(true)} diff --git a/ui/components/invitations/table/skeleton-table-invitations.tsx b/ui/components/invitations/table/skeleton-table-invitations.tsx index 15645af2a4..1117358e32 100644 --- a/ui/components/invitations/table/skeleton-table-invitations.tsx +++ b/ui/components/invitations/table/skeleton-table-invitations.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 SkeletonTableInvitation = () => { return ( - + {/* Table headers */} -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
+
+ + + + + +
{/* Table body */} @@ -32,26 +21,14 @@ export const SkeletonTableInvitation = () => { {[...Array(10)].map((_, index) => (
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
+ + + + + +
))}
diff --git a/ui/components/invitations/workflow/forms/send-invitation-form.tsx b/ui/components/invitations/workflow/forms/send-invitation-form.tsx index 6ab173a4f6..7840467fe6 100644 --- a/ui/components/invitations/workflow/forms/send-invitation-form.tsx +++ b/ui/components/invitations/workflow/forms/send-invitation-form.tsx @@ -8,8 +8,9 @@ import { Controller, useForm } from "react-hook-form"; import * as z from "zod"; import { sendInvite } from "@/actions/invitations/invitation"; +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 { ApiError } from "@/types"; @@ -104,7 +105,6 @@ export const SendInvitationForm = ({ placeholder="Enter the email address" variant="flat" isRequired - isInvalid={!!form.formState.errors.email} /> {form.formState.errors.roleId && ( -

+

{form.formState.errors.roleId.message}

)} @@ -145,18 +145,22 @@ export const SendInvitationForm = ({ {/* Submit Button */}
- } + disabled={isLoading} > - {isLoading ? <>Loading : Send Invitation} - + {isLoading ? ( + <>Loading + ) : ( + <> + + Send Invitation + + )} +
diff --git a/ui/components/invitations/workflow/vertical-steps.tsx b/ui/components/invitations/workflow/vertical-steps.tsx index 405d33f631..28fce93361 100644 --- a/ui/components/invitations/workflow/vertical-steps.tsx +++ b/ui/components/invitations/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. */ @@ -123,7 +128,7 @@ export const VerticalSteps = React.forwardRef< switch (color) { case "primary": - userColor = "[--step-color:hsl(var(--heroui-primary))]"; + userColor = "[--step-color:var(--bg-button-primary)]"; fgColor = "[--step-fg-color:hsl(var(--heroui-primary-foreground))]"; break; case "secondary": diff --git a/ui/components/invitations/workflow/workflow-send-invite.tsx b/ui/components/invitations/workflow/workflow-send-invite.tsx index 1c9d2ffbee..9183c27a8d 100644 --- a/ui/components/invitations/workflow/workflow-send-invite.tsx +++ b/ui/components/invitations/workflow/workflow-send-invite.tsx @@ -44,13 +44,14 @@ export const WorkflowSendInvite = () => { base: "px-0.5 mb-3 sm:mb-5", label: "text-xs sm:text-small", value: "text-xs sm:text-small text-default-400", + indicator: "bg-button-primary", }} label="Steps" - maxValue={steps.length - 1} + maxValue={steps.length} minValue={0} showValueLabel={true} size="sm" - value={currentStep} + value={currentStep + 1} valueLabel={`${currentStep + 1} of ${steps.length}`} /> @@ -59,14 +60,14 @@ export const WorkflowSendInvite = () => {
{/* Mobile: Compact current step indicator */}
-
+
Current: {steps[currentStep]?.title}
diff --git a/ui/components/lighthouse/ai-elements/actions.tsx b/ui/components/lighthouse/ai-elements/actions.tsx index e1f9abfb5b..900f7409c6 100644 --- a/ui/components/lighthouse/ai-elements/actions.tsx +++ b/ui/components/lighthouse/ai-elements/actions.tsx @@ -2,9 +2,9 @@ import type { ComponentProps } from "react"; +import { Button } from "@/components/shadcn/button/button"; import { cn } from "@/lib/utils"; -import { Button } from "./button"; import { Tooltip, TooltipContent, diff --git a/ui/components/lighthouse/ai-elements/button.tsx b/ui/components/lighthouse/ai-elements/button.tsx deleted file mode 100644 index cb0ecfbcf3..0000000000 --- a/ui/components/lighthouse/ai-elements/button.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import * as React 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", - { - 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", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/ui/components/lighthouse/ai-elements/dropdown-menu.tsx b/ui/components/lighthouse/ai-elements/dropdown-menu.tsx index 220c3fde4e..b1df13e7f5 100644 --- a/ui/components/lighthouse/ai-elements/dropdown-menu.tsx +++ b/ui/components/lighthouse/ai-elements/dropdown-menu.tsx @@ -2,7 +2,6 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; -import * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/ui/components/lighthouse/ai-elements/input-group.tsx b/ui/components/lighthouse/ai-elements/input-group.tsx index 2db40406e8..e96d61140d 100644 --- a/ui/components/lighthouse/ai-elements/input-group.tsx +++ b/ui/components/lighthouse/ai-elements/input-group.tsx @@ -1,11 +1,10 @@ "use client"; import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; +import { Button } from "@/components/shadcn/button/button"; import { cn } from "@/lib/utils"; -import { Button } from "./button"; import { Input } from "./input"; import { Textarea } from "./textarea"; @@ -15,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="input-group" role="group" className={cn( - "group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none", + "group/input-group border-border-neutral-secondary bg-bg-neutral-secondary relative flex w-full items-center rounded-xl border transition-[color,box-shadow] outline-none", "h-9 min-w-0 has-[>textarea]:h-auto", // Variants based on alignment. @@ -25,7 +24,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3", // Focus state. - "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]", + "has-[[data-slot=input-group-control]:focus-visible]:border-button-primary has-[[data-slot=input-group-control]:focus-visible]:ring-button-primary/20 has-[[data-slot=input-group-control]:focus-visible]:ring-2", // Error state. "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", diff --git a/ui/components/lighthouse/ai-elements/input.tsx b/ui/components/lighthouse/ai-elements/input.tsx index f16c2c0ec7..ebfe595935 100644 --- a/ui/components/lighthouse/ai-elements/input.tsx +++ b/ui/components/lighthouse/ai-elements/input.tsx @@ -1,5 +1,3 @@ -import * as React from "react"; - import { cn } from "@/lib/utils"; function Input({ className, type, ...props }: React.ComponentProps<"input">) { diff --git a/ui/components/lighthouse/ai-elements/prompt-input.tsx b/ui/components/lighthouse/ai-elements/prompt-input.tsx index a1856b3073..32f4f6a9b5 100644 --- a/ui/components/lighthouse/ai-elements/prompt-input.tsx +++ b/ui/components/lighthouse/ai-elements/prompt-input.tsx @@ -1,5 +1,3 @@ -/** biome-ignore-all lint/performance/noImgElement: "AI Elements is framework agnostic" */ - "use client"; import type { ChatStatus, FileUIPart } from "ai"; @@ -37,9 +35,9 @@ import { useState, } from "react"; +import { Button } from "@/components/shadcn/button/button"; import { cn } from "@/lib/utils"; -import { Button } from "./button"; import { DropdownMenu, DropdownMenuContent, diff --git a/ui/components/lighthouse/ai-elements/select.tsx b/ui/components/lighthouse/ai-elements/select.tsx index 0f2c26124b..030eeca2c7 100644 --- a/ui/components/lighthouse/ai-elements/select.tsx +++ b/ui/components/lighthouse/ai-elements/select.tsx @@ -2,7 +2,6 @@ import * as SelectPrimitive from "@radix-ui/react-select"; import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import * as React from "react"; import { cn } from "@/lib/utils"; diff --git a/ui/components/lighthouse/ai-elements/textarea.tsx b/ui/components/lighthouse/ai-elements/textarea.tsx index 0735a8ca69..f8a7e43014 100644 --- a/ui/components/lighthouse/ai-elements/textarea.tsx +++ b/ui/components/lighthouse/ai-elements/textarea.tsx @@ -1,5 +1,3 @@ -import * as React from "react"; - import { cn } from "@/lib/utils"; function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { diff --git a/ui/components/lighthouse/banner-client.tsx b/ui/components/lighthouse/banner-client.tsx new file mode 100644 index 0000000000..9c91f2555a --- /dev/null +++ b/ui/components/lighthouse/banner-client.tsx @@ -0,0 +1,128 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; + +import { Card, CardContent } from "@/components/shadcn/card/card"; +import { cn } from "@/lib/utils"; + +import { LighthouseIcon } from "../icons"; + +const AnimatedGradientCard = ({ + message, + href, +}: { + message: string; + href: string; +}) => { + const interactiveRef = useRef(null); + const curXRef = useRef(0); + const curYRef = useRef(0); + const tgXRef = useRef(0); + const tgYRef = useRef(0); + const [isSafari, setIsSafari] = useState(false); + + useEffect(() => { + setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent)); + }, []); + + useEffect(() => { + let animationFrameId: number; + + const move = () => { + if (!interactiveRef.current) return; + + curXRef.current += (tgXRef.current - curXRef.current) / 20; + curYRef.current += (tgYRef.current - curYRef.current) / 20; + + interactiveRef.current.style.transform = `translate(${Math.round(curXRef.current)}px, ${Math.round(curYRef.current)}px)`; + + animationFrameId = requestAnimationFrame(move); + }; + + animationFrameId = requestAnimationFrame(move); + + return () => { + cancelAnimationFrame(animationFrameId); + }; + }, []); + + const handleMouseMove = (event: React.MouseEvent) => { + if (interactiveRef.current) { + const rect = interactiveRef.current.getBoundingClientRect(); + tgXRef.current = event.clientX - rect.left; + tgYRef.current = event.clientY - rect.top; + } + }; + + return ( + + + + + + + + + + + + +
+
+ +
+ +
+ +
+
+ + +
+
+ +
+

+ {message} +

+
+
+ + + ); +}; + +export const LighthouseBannerClient = ({ + isConfigured, +}: { + isConfigured: boolean; +}) => { + const message = isConfigured + ? "Use Lighthouse to review your findings and gain insights" + : "Enable Lighthouse to secure your cloud with AI insights"; + const href = isConfigured ? "/lighthouse" : "/lighthouse/config"; + + return ; +}; diff --git a/ui/components/lighthouse/banner.tsx b/ui/components/lighthouse/banner.tsx index b3567b2792..8e8719bf03 100644 --- a/ui/components/lighthouse/banner.tsx +++ b/ui/components/lighthouse/banner.tsx @@ -1,52 +1,13 @@ -import { Bot } from "lucide-react"; -import Link from "next/link"; - import { isLighthouseConfigured } from "@/actions/lighthouse/lighthouse"; -interface BannerConfig { - message: string; - href: string; - gradient: string; -} - -const renderBanner = ({ message, href, gradient }: BannerConfig) => ( - -
-
-
-
- -
-

{message}

-
-
-
- -); +import { LighthouseBannerClient } from "./banner-client"; export const LighthouseBanner = async () => { try { const isConfigured = await isLighthouseConfigured(); - if (!isConfigured) { - return renderBanner({ - message: "Enable Lighthouse to secure your cloud with AI insights", - href: "/lighthouse/config", - gradient: - "bg-linear-to-r from-green-500 to-blue-500 hover:from-green-600 hover:to-blue-600 focus:ring-green-500/50 dark:from-green-600 dark:to-blue-600 dark:hover:from-green-700 dark:hover:to-blue-700 dark:focus:ring-green-400/50", - }); - } else { - return renderBanner({ - message: "Use Lighthouse to review your findings and gain insights", - href: "/lighthouse", - gradient: - "bg-linear-to-r from-green-500 to-blue-500 hover:from-green-600 hover:to-blue-600 focus:ring-green-500/50 dark:from-green-600 dark:to-blue-600 dark:hover:from-green-700 dark:hover:to-blue-700 dark:focus:ring-green-400/50", - }); - } + return ; } catch (error) { - console.error("Error getting banner state:", error); return null; } }; diff --git a/ui/components/lighthouse/chat.tsx b/ui/components/lighthouse/chat.tsx index bd2d5d8584..30bfd5022d 100644 --- a/ui/components/lighthouse/chat.tsx +++ b/ui/components/lighthouse/chat.tsx @@ -2,17 +2,12 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { ChevronDown, Copy, Plus, RotateCcw } from "lucide-react"; +import { Copy, Plus, RotateCcw } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Streamdown } from "streamdown"; import { getLighthouseModelIds } from "@/actions/lighthouse/lighthouse"; import { Action, Actions } from "@/components/lighthouse/ai-elements/actions"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "@/components/lighthouse/ai-elements/dropdown-menu"; import { PromptInput, PromptInputBody, @@ -22,10 +17,17 @@ import { PromptInputTools, } from "@/components/lighthouse/ai-elements/prompt-input"; import { Loader } from "@/components/lighthouse/loader"; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Combobox, +} from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; import { CustomLink } from "@/components/ui/custom/custom-link"; -import { cn } from "@/lib/utils"; import type { LighthouseProvider } from "@/types/lighthouse"; interface Model { @@ -92,14 +94,8 @@ export const Chat = ({ // Consolidated UI state const [uiState, setUiState] = useState<{ inputValue: string; - isDropdownOpen: boolean; - modelSearchTerm: string; - hoveredProvider: LighthouseProvider | ""; }>({ inputValue: "", - isDropdownOpen: false, - modelSearchTerm: "", - hoveredProvider: defaultProviderId || initialProviders[0]?.id || "", }); // Error handling @@ -107,13 +103,7 @@ export const Chat = ({ // Provider and model management const [providers, setProviders] = useState(initialProviders); - const [providerLoadState, setProviderLoadState] = useState<{ - loaded: Set; - loading: Set; - }>({ - loaded: new Set(), - loading: new Set(), - }); + const loadedProvidersRef = useRef>(new Set()); // Initialize selectedModel with defaults from props const [selectedModel, setSelectedModel] = useState(() => { @@ -135,20 +125,23 @@ export const Chat = ({ const selectedModelRef = useRef(selectedModel); selectedModelRef.current = selectedModel; + // Load models for all providers on mount + useEffect(() => { + initialProviders.forEach((provider) => { + loadModelsForProvider(provider.id); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Load all models for a specific provider const loadModelsForProvider = async (providerType: LighthouseProvider) => { - setProviderLoadState((prev) => { - // Skip if already loaded or currently loading - if (prev.loaded.has(providerType) || prev.loading.has(providerType)) { - return prev; - } + // Skip if already loaded + if (loadedProvidersRef.current.has(providerType)) { + return; + } - // Mark as loading - return { - ...prev, - loading: new Set([...Array.from(prev.loading), providerType]), - }; - }); + // Mark as loaded + loadedProvidersRef.current.add(providerType); try { const response = await getLighthouseModelIds(providerType); @@ -169,24 +162,11 @@ export const Chat = ({ setProviders((prev) => prev.map((p) => (p.id === providerType ? { ...p, models } : p)), ); - - // Mark as loaded and remove from loading - setProviderLoadState((prev) => ({ - loaded: new Set([...Array.from(prev.loaded), providerType]), - loading: new Set( - Array.from(prev.loading).filter((id) => id !== providerType), - ), - })); } } catch (error) { console.error(`Error loading models for ${providerType}:`, error); - // Remove from loading state on error - setProviderLoadState((prev) => ({ - ...prev, - loading: new Set( - Array.from(prev.loading).filter((id) => id !== providerType), - ), - })); + // Remove from loaded on error so it can be retried + loadedProvidersRef.current.delete(providerType); } }; @@ -292,22 +272,6 @@ export const Chat = ({ } }, [messages, status]); - // Handle dropdown state changes - useEffect(() => { - if (uiState.isDropdownOpen && uiState.hoveredProvider) { - loadModelsForProvider(uiState.hoveredProvider as LighthouseProvider); - } - }, [uiState.isDropdownOpen, uiState.hoveredProvider, loadModelsForProvider]); - - // Filter models based on search term - const currentProvider = providers.find( - (p) => p.id === uiState.hoveredProvider, - ); - const filteredModels = - currentProvider?.models.filter((model) => - model.name.toLowerCase().includes(uiState.modelSearchTerm.toLowerCase()), - ) || []; - // Handlers const handleNewChat = () => { setMessages([]); @@ -321,61 +285,62 @@ export const Chat = ({ modelName: string, ) => { setSelectedModel({ providerType, modelId, modelName }); - setUiState((prev) => ({ - ...prev, - isDropdownOpen: false, - modelSearchTerm: "", // Reset search when selecting - })); }; return ( -
+
{/* Header with New Chat button */} {messages.length > 0 && ( -
+
- } - onPress={handleNewChat} + onClick={handleNewChat} className="gap-1" > + New Chat - +
)} {!hasConfig && (
-
-

- LLM Provider Configuration Required -

-

- Please configure an LLM provider to use Lighthouse AI. -

- - Configure Provider - -
+ + + LLM Provider Configuration Required + + Please configure an LLM provider to use Lighthouse AI. + + + + + Configure Provider + + +
)} {/* Error Banner */} {(error || errorMessage) && ( -
+
@@ -387,26 +352,24 @@ export const Chat = ({
-

- Error -

-

+

Error

+

{errorMessage || error?.message || "An error occurred. Please retry your message."}

{/* Original error details for native errors */} {error && (error as any).status && ( -

+

Status: {(error as any).status}

)} {error && (error as any).body && (
- + Show details -
+                  
                     {JSON.stringify((error as any).body, null, 2)}
                   
@@ -417,31 +380,32 @@ export const Chat = ({ )} {messages.length === 0 && !errorMessage && !error ? ( -
+

Suggestions

{SUGGESTED_ACTIONS.map((action, index) => ( - { + aria-label={`Send message: ${action.action}`} + onClick={() => { sendMessage({ text: action.action, }); }} - className="hover:bg-muted flex h-auto w-full flex-col items-start justify-start rounded-xl border bg-gray-50 px-4 py-3.5 text-left font-sans text-sm dark:bg-gray-900" + variant="outline" + className="flex h-auto w-full flex-col items-start justify-start rounded-xl px-4 py-3.5 text-left font-sans text-sm" > {action.title} {action.label} - + ))}
) : (
{messages.map((message, idx) => { @@ -470,7 +434,7 @@ export const Chat = ({
@@ -478,11 +442,7 @@ export const Chat = ({ {isStreamingAssistant && !messageText ? ( ) : ( -
+
- {/* Model Selector */} - - setUiState((prev) => ({ ...prev, isDropdownOpen: open })) - } - > - - - - -
- {/* Left column - Providers */} -
- {providers.map((provider) => ( -
{ - setUiState((prev) => ({ - ...prev, - hoveredProvider: provider.id, - modelSearchTerm: "", // Reset search when changing provider - })); - loadModelsForProvider(provider.id); - }} - className={cn( - "flex cursor-default items-center justify-between rounded-sm px-3 py-2 text-sm transition-colors", - uiState.hoveredProvider === provider.id - ? "bg-gray-100 dark:bg-gray-800" - : "hover:ring-default-200 dark:hover:ring-default-700 hover:bg-gray-100 hover:ring-1 dark:hover:bg-gray-800", - )} - > - {provider.name} - -
- ))} -
- - {/* Right column - Models */} -
- {/* Search bar */} -
- - setUiState((prev) => ({ - ...prev, - modelSearchTerm: e.target.value, - })) - } - className="placeholder:text-muted-foreground w-full rounded-md border border-gray-200 bg-transparent px-3 py-1.5 text-sm outline-hidden focus:border-gray-400 dark:border-gray-700 dark:focus:border-gray-500" - /> -
- - {/* Models list */} -
- {uiState.hoveredProvider && - providerLoadState.loading.has( - uiState.hoveredProvider as LighthouseProvider, - ) ? ( -
- -
- ) : filteredModels.length === 0 ? ( -
- {uiState.modelSearchTerm - ? "No models found" - : "No models available"} -
- ) : ( - filteredModels.map((model) => ( - - )) - )} -
-
-
-
-
+ {/* Model Selector - Combobox */} + { + const [providerType, modelId] = value.split(":"); + const provider = providers.find((p) => p.id === providerType); + const model = provider?.models.find((m) => m.id === modelId); + if (provider && model) { + handleModelSelect( + providerType as LighthouseProvider, + modelId, + model.name, + ); + } + }} + groups={providers.map((provider) => ({ + heading: provider.name, + options: provider.models.map((model) => ({ + value: `${provider.id}:${model.id}`, + label: model.name, + })), + }))} + placeholder={selectedModel.modelName || "Select model..."} + searchPlaceholder="Search models..." + emptyMessage="No model found." + showSelectedFirst={true} + />
{/* Submit Button */} diff --git a/ui/components/lighthouse/connect-llm-provider.tsx b/ui/components/lighthouse/connect-llm-provider.tsx index 2cfeee261d..50ed9ec063 100644 --- a/ui/components/lighthouse/connect-llm-provider.tsx +++ b/ui/components/lighthouse/connect-llm-provider.tsx @@ -8,7 +8,7 @@ import { getLighthouseProviderByType, updateLighthouseProviderByType, } from "@/actions/lighthouse/lighthouse"; -import { CustomButton } from "@/components/ui/custom"; +import { FormButtons } from "@/components/ui/form"; import type { LighthouseProvider } from "@/types/lighthouse"; import { getMainFields, getProviderConfig } from "./llm-provider-registry"; @@ -88,7 +88,8 @@ export const ConnectLLMProvider = ({ }; }; - const handleConnect = async () => { + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); if (!providerConfig) return; setStatus("connecting"); @@ -146,25 +147,26 @@ export const ConnectLLMProvider = ({ if (error) setError(null); }; - const getButtonText = () => { - if (status === "idle") { - if (error && existingProviderId) return "Retry Connection"; - return isEditMode ? "Continue" : "Connect"; - } + const getSubmitText = () => { + if (error && existingProviderId) return "Retry Connection"; + return isEditMode ? "Continue" : "Connect"; + }; - const statusText = { + const getLoadingText = () => { + if (status === "idle") return "Connecting..."; + + const statusText: Record, string> = { connecting: "Connecting...", verifying: "Verifying...", "loading-models": "Loading models...", }; - return statusText[status] || "Connecting..."; }; if (!providerConfig) { return (
-
+
Provider configuration not found: {provider}
@@ -174,7 +176,7 @@ export const ConnectLLMProvider = ({ if (isFetching) { return (
-
+
Loading provider configuration...
@@ -193,7 +195,7 @@ export const ConnectLLMProvider = ({ ? `Update ${providerConfig.name}` : `Connect to ${providerConfig.name}`} -

+

{isEditMode ? `Update your API credentials or settings for ${providerConfig.name}.` : `Enter your API credentials to connect to ${providerConfig.name}.`} @@ -201,12 +203,12 @@ export const ConnectLLMProvider = ({

{error && ( -
-

{error}

+
+

{error}

)} -
+
{mainFields.map((field) => (
))} -
- router.push("/lighthouse/config")} - isDisabled={isLoading} - > - Cancel - - - {getButtonText()} - -
-
+ router.push("/lighthouse/config")} + submitText={getSubmitText()} + loadingText={getLoadingText()} + isDisabled={!isFormValid || isLoading} + /> +
); }; diff --git a/ui/components/lighthouse/forms/delete-llm-provider-form.tsx b/ui/components/lighthouse/forms/delete-llm-provider-form.tsx index 1666ee5cc6..61d60369fa 100644 --- a/ui/components/lighthouse/forms/delete-llm-provider-form.tsx +++ b/ui/components/lighthouse/forms/delete-llm-provider-form.tsx @@ -9,8 +9,7 @@ import * as z from "zod"; import { deleteLighthouseProviderByType } from "@/actions/lighthouse/lighthouse"; import { DeleteIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; +import { Form, FormButtons } from "@/components/ui/form"; import type { LighthouseProvider } from "@/types/lighthouse"; const formSchema = z.object({ @@ -57,34 +56,19 @@ export const DeleteLLMProviderForm = ({ return (
- -
- setIsOpen(false)} - isDisabled={isLoading} - > - Cancel - - - } - > - {isLoading ? <>Loading : Delete} - -
+ + } + isDisabled={isLoading} + /> ); diff --git a/ui/components/lighthouse/lighthouse-settings.tsx b/ui/components/lighthouse/lighthouse-settings.tsx index e497117025..4fcd1afc86 100644 --- a/ui/components/lighthouse/lighthouse-settings.tsx +++ b/ui/components/lighthouse/lighthouse-settings.tsx @@ -2,7 +2,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Icon } from "@iconify/react"; -import { SaveIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -11,8 +10,16 @@ import { getTenantConfig, updateTenantConfig, } from "@/actions/lighthouse/lighthouse"; +import { SaveIcon } from "@/components/icons"; +import { + Button, + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/shadcn"; import { useToast } from "@/components/ui"; -import { CustomButton, CustomTextarea } from "@/components/ui/custom"; +import { CustomTextarea } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; const lighthouseSettingsSchema = z.object({ @@ -97,55 +104,58 @@ export const LighthouseSettings = () => { if (isFetchingData) { return ( -
-

Settings

-
- -
-
+ + + Settings + + +
+ +
+
+
); } return ( -
-

Settings

+ + + Settings + + +
+ + - - - - -
- } - > - {isLoading ? "Saving..." : "Save"} - -
- - -
+
+ +
+ + + + ); }; diff --git a/ui/components/lighthouse/llm-providers-table.tsx b/ui/components/lighthouse/llm-providers-table.tsx index 7b3d48b47b..4ca3f9e084 100644 --- a/ui/components/lighthouse/llm-providers-table.tsx +++ b/ui/components/lighthouse/llm-providers-table.tsx @@ -1,13 +1,14 @@ "use client"; import { Icon } from "@iconify/react"; +import Link from "next/link"; import { useEffect, useState } from "react"; import { getLighthouseProviders, getTenantConfig, } from "@/actions/lighthouse/lighthouse"; -import { CustomButton } from "@/components/ui/custom"; +import { Button, Card, CardContent, CardHeader } from "@/components/shadcn"; import { getAllProviders } from "./llm-provider-registry"; @@ -104,31 +105,32 @@ export const LLMProvidersTable = () => {

LLM Providers

{[1, 2, 3].map((i) => ( -
-
-
-
-
-
+ + +
+
+
+
+
+
-
+ -
-
-
-
+ +
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+ + ))}
@@ -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 && ( + )} -
- {/* Action Button */} - {showConnect && ( - - Connect - - )} - - {showConfigure && ( - - Configure - - )} -
+ {showConfigure && ( + + )} + + ); })}
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 = ({

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} - +
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 = ({
-
- + - } + disabled={isLoading} > - {isLoading ? <>Loading : Delete} - + {!isLoading && } + {isLoading ? "Loading" : "Delete"} +
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 = ({

)} -
- + +
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 - + ); }; 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 ( - - -
- - - } /> - - - - - -
-
-
- - } - 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 - +
); }; 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 - + ); }; 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}
@@ -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} /> -
- + - } + 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 - - +
); @@ -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 - + ); }; 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} + ); }; 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 = ({

- - CloudFormation {integrationType ? "" : "Template"} - - + CloudFormation {integrationType ? "" : "Template"} + + +
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} + ) : ( - } + 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 ( - - ); - })} -
- ) : ( -

- No findings found for this resource. -

- )} - + ); + } + + const { severity, check_metadata, status } = findingAttrs; + const checktitle = + check_metadata?.checktitle || "Unknown check"; + + return ( + + ); + })} +
+ ) : ( +

+ 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 = ({
-
- + - } + disabled={isLoading} > - {isLoading ? <>Loading : Delete} - + {!isLoading && } + {isLoading ? "Loading" : "Delete"} +
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.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 = < ( - <> - - - - - - )} + render={({ field }) => { + const selectedItem = providers.find( + (item) => item.providerId === field.value, + ); + + return ( +
+ + Select a cloud provider to launch a scan + + + + + +
+ ); + }} /> ); }; 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 - -
-
-
+ + +
); }; 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 ( + + + + + + + + + {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 ( + + + + ); +} + +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 ? ( + + ) : ( + ) : (
@@ -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 ( -
-
-
- - {emptyText} - -
-
-
- ); - } - - 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} +

+
+
+
-

{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, - ) => ( - - ), -); - -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 ( -
- - - - - -
- - {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 ( -
- - - - - -
- - - 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 ( -
- +
); }; 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 ( + + ); + } + return ( - // TODO: Replace CustomButton with CustomLink once the CustomLink component is merged. - - {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 (