mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat: add watchlist component (#9199)
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
137
ui/actions/compliances/compliances.adapter.ts
Normal file
137
ui/actions/compliances/compliances.adapter.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { StaticImageData } from "next/image";
|
||||
|
||||
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
|
||||
import { MetaDataProps } from "@/types";
|
||||
import { ComplianceOverviewData } from "@/types/compliance";
|
||||
|
||||
/**
|
||||
* Raw API response from /compliance-overviews endpoint
|
||||
*/
|
||||
export interface ComplianceOverviewsResponse {
|
||||
data: ComplianceOverviewData[];
|
||||
meta?: {
|
||||
pagination?: {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriched compliance overview with computed fields
|
||||
*/
|
||||
export interface EnrichedComplianceOverview {
|
||||
id: string;
|
||||
framework: string;
|
||||
version: string;
|
||||
requirements_passed: number;
|
||||
requirements_failed: number;
|
||||
requirements_manual: number;
|
||||
total_requirements: number;
|
||||
score: number;
|
||||
label: string;
|
||||
icon: string | StaticImageData | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats framework name for display by replacing hyphens with spaces
|
||||
* e.g., "FedRAMP-20x-KSI-Low" -> "FedRAMP 20x KSI Low"
|
||||
*/
|
||||
function formatFrameworkName(framework: string): string {
|
||||
return framework.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts the raw API response to enriched compliance data
|
||||
* - Computes score percentage (rounded)
|
||||
* - Formats label (framework + version)
|
||||
* - Resolves framework icon
|
||||
* - Preserves pagination metadata
|
||||
*
|
||||
* @param response - Raw API response with data and optional pagination
|
||||
* @returns Object with enriched compliance data and metadata
|
||||
*/
|
||||
export function adaptComplianceOverviewsResponse(
|
||||
response: ComplianceOverviewsResponse | undefined,
|
||||
): {
|
||||
data: EnrichedComplianceOverview[];
|
||||
metadata?: MetaDataProps;
|
||||
} {
|
||||
if (!response?.data) {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
const enrichedData = response.data.map((compliance) => {
|
||||
const { id, attributes } = compliance;
|
||||
const {
|
||||
framework,
|
||||
version,
|
||||
requirements_passed,
|
||||
requirements_failed,
|
||||
requirements_manual,
|
||||
total_requirements,
|
||||
} = attributes;
|
||||
|
||||
const totalRequirements = Number(total_requirements) || 0;
|
||||
const passedRequirements = Number(requirements_passed) || 0;
|
||||
|
||||
const score =
|
||||
totalRequirements > 0
|
||||
? Math.round((passedRequirements / totalRequirements) * 100)
|
||||
: 0;
|
||||
|
||||
const formattedFramework = formatFrameworkName(framework);
|
||||
const label = version
|
||||
? `${formattedFramework} - ${version}`
|
||||
: formattedFramework;
|
||||
const icon = getComplianceIcon(framework);
|
||||
|
||||
return {
|
||||
id,
|
||||
framework,
|
||||
version,
|
||||
requirements_passed,
|
||||
requirements_failed,
|
||||
requirements_manual,
|
||||
total_requirements,
|
||||
score,
|
||||
label,
|
||||
icon,
|
||||
};
|
||||
});
|
||||
|
||||
const metadata: MetaDataProps | undefined = response.meta?.pagination
|
||||
? {
|
||||
pagination: {
|
||||
page: response.meta.pagination.page,
|
||||
pages: response.meta.pagination.pages,
|
||||
count: response.meta.pagination.count,
|
||||
itemsPerPage: [10, 25, 50, 100],
|
||||
},
|
||||
version: "1.0",
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return { data: enrichedData, metadata };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts compliances for watchlist display:
|
||||
* - Excludes ProwlerThreatScore
|
||||
* - Sorted by score ascending (worst/lowest scores first)
|
||||
* - Limited to specified count
|
||||
*
|
||||
* @param data - Enriched compliance data
|
||||
* @param limit - Maximum number of items to return (default: 9)
|
||||
* @returns Sorted and limited compliance data
|
||||
*/
|
||||
export function sortCompliancesForWatchlist(
|
||||
data: EnrichedComplianceOverview[],
|
||||
limit: number = 9,
|
||||
): EnrichedComplianceOverview[] {
|
||||
return [...data]
|
||||
.filter((item) => item.framework !== "ProwlerThreatScore")
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.slice(0, limit);
|
||||
}
|
||||
@@ -7,22 +7,31 @@ export const getCompliancesOverview = async ({
|
||||
scanId,
|
||||
region,
|
||||
query,
|
||||
filters = {},
|
||||
}: {
|
||||
scanId: string;
|
||||
scanId?: string;
|
||||
region?: string | string[];
|
||||
query?: string;
|
||||
}) => {
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
} = {}) => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/compliance-overviews`);
|
||||
|
||||
if (scanId) url.searchParams.append("filter[scan_id]", scanId);
|
||||
if (query) url.searchParams.append("filter[search]", query);
|
||||
const setParam = (key: string, value?: string | string[]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (region) {
|
||||
const regionValue = Array.isArray(region) ? region.join(",") : region;
|
||||
url.searchParams.append("filter[region__in]", regionValue);
|
||||
}
|
||||
const serializedValue = Array.isArray(value) ? value.join(",") : value;
|
||||
if (serializedValue.trim().length > 0) {
|
||||
url.searchParams.set(key, serializedValue);
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(filters).forEach(([key, value]) => setParam(key, value));
|
||||
|
||||
setParam("filter[scan_id]", scanId);
|
||||
setParam("filter[region__in]", region);
|
||||
if (query) url.searchParams.set("filter[search]", query);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
@@ -31,7 +40,7 @@ export const getCompliancesOverview = async ({
|
||||
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching providers:", error);
|
||||
console.error("Error fetching compliances overview:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./compliances";
|
||||
export * from "./compliances.adapter";
|
||||
|
||||
@@ -34,7 +34,7 @@ export const FindingSeverityOverTimeSSR = async ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="base" className="flex h-full flex-col">
|
||||
<Card variant="base" className="flex h-full flex-1 flex-col">
|
||||
<CardHeader className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Finding Severity Over Time</CardTitle>
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Skeleton,
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
interface FindingsData {
|
||||
total: number;
|
||||
new: number;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
adaptComplianceOverviewsResponse,
|
||||
getCompliancesOverview,
|
||||
} from "@/actions/compliances";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { pickFilterParams } from "../../lib/filter-params";
|
||||
import { ComplianceWatchlist } from "./compliance-watchlist";
|
||||
|
||||
export const ComplianceWatchlistSSR = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps | undefined | null;
|
||||
}) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const response = await getCompliancesOverview({ filters });
|
||||
const { data } = adaptComplianceOverviewsResponse(response);
|
||||
|
||||
// Filter out ProwlerThreatScore and limit to 9 items
|
||||
const items = data
|
||||
.filter((item) => item.framework !== "ProwlerThreatScore")
|
||||
.slice(0, 9)
|
||||
.map((compliance) => ({
|
||||
id: compliance.id,
|
||||
framework: compliance.framework,
|
||||
label: compliance.label,
|
||||
icon: compliance.icon,
|
||||
score: compliance.score,
|
||||
}));
|
||||
|
||||
return <ComplianceWatchlist items={items} />;
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowDownNarrowWide, ArrowUpNarrowWide } from "lucide-react";
|
||||
import Image, { type StaticImageData } from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
|
||||
import { WatchlistCard } from "./watchlist-card";
|
||||
|
||||
export interface ComplianceData {
|
||||
id: string;
|
||||
framework: string;
|
||||
label: string;
|
||||
icon?: string | StaticImageData;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export const ComplianceWatchlist = ({ items }: { items: ComplianceData[] }) => {
|
||||
const [isAsc, setIsAsc] = useState(true);
|
||||
|
||||
const sortedItems = [...items]
|
||||
.sort((a, b) => (isAsc ? a.score - b.score : b.score - a.score))
|
||||
.map((item) => ({
|
||||
key: item.id,
|
||||
icon: item.icon ? (
|
||||
<div className="relative size-3">
|
||||
<Image
|
||||
src={item.icon}
|
||||
alt={`${item.framework} framework`}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-bg-data-muted size-3 rounded-sm" />
|
||||
),
|
||||
label: item.label,
|
||||
value: `${item.score}%`,
|
||||
}));
|
||||
|
||||
const SortIcon = isAsc ? ArrowUpNarrowWide : ArrowDownNarrowWide;
|
||||
|
||||
return (
|
||||
<WatchlistCard
|
||||
title="Compliance Watchlist"
|
||||
items={sortedItems}
|
||||
ctaLabel="Compliance Dashboard"
|
||||
ctaHref="/compliance"
|
||||
headerAction={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsAsc(!isAsc)}
|
||||
aria-label={isAsc ? "Sort by highest score" : "Sort by lowest score"}
|
||||
>
|
||||
<SortIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
emptyState={{
|
||||
message: "This space is looking empty.",
|
||||
description: "to add compliance frameworks to your watchlist.",
|
||||
linkText: "Compliance Dashboard",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type { ComplianceData } from "./compliance-watchlist";
|
||||
export { ComplianceWatchlist } from "./compliance-watchlist";
|
||||
export { ComplianceWatchlistSSR } from "./compliance-watchlist.ssr";
|
||||
export * from "./service-watchlist";
|
||||
export * from "./watchlist-card";
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { getAWSIcon } from "@/components/icons/services/IconServices";
|
||||
|
||||
import { WatchlistCard, WatchlistItem } from "./watchlist-card";
|
||||
|
||||
const MOCK_SERVICE_ITEMS: WatchlistItem[] = [
|
||||
{
|
||||
key: "amazon-s3-1",
|
||||
icon: getAWSIcon("Amazon S3"),
|
||||
label: "Amazon S3",
|
||||
value: "5",
|
||||
},
|
||||
{
|
||||
key: "amazon-ec2",
|
||||
icon: getAWSIcon("Amazon EC2"),
|
||||
label: "Amazon EC2",
|
||||
value: "8",
|
||||
},
|
||||
{
|
||||
key: "amazon-rds",
|
||||
icon: getAWSIcon("Amazon RDS"),
|
||||
label: "Amazon RDS",
|
||||
value: "12",
|
||||
},
|
||||
{
|
||||
key: "aws-iam",
|
||||
icon: getAWSIcon("AWS IAM"),
|
||||
label: "AWS IAM",
|
||||
value: "15",
|
||||
},
|
||||
{
|
||||
key: "aws-lambda",
|
||||
icon: getAWSIcon("AWS Lambda"),
|
||||
label: "AWS Lambda",
|
||||
value: "22",
|
||||
},
|
||||
{
|
||||
key: "amazon-vpc",
|
||||
icon: getAWSIcon("Amazon VPC"),
|
||||
label: "Amazon VPC",
|
||||
value: "28",
|
||||
},
|
||||
{
|
||||
key: "amazon-cloudwatch",
|
||||
icon: getAWSIcon("AWS CloudWatch"),
|
||||
label: "AWS CloudWatch",
|
||||
value: "78",
|
||||
},
|
||||
];
|
||||
|
||||
export const ServiceWatchlist = () => {
|
||||
return (
|
||||
<WatchlistCard
|
||||
title="Service Watchlist"
|
||||
items={MOCK_SERVICE_ITEMS}
|
||||
ctaLabel="Services Dashboard"
|
||||
ctaHref="/services"
|
||||
emptyState={{
|
||||
message: "This space is looking empty.",
|
||||
description: "to add services to your watchlist.",
|
||||
linkText: "Services Dashboard",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
import { SearchX } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
} from "@/components/shadcn/card/card";
|
||||
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SCORE_CONFIG = {
|
||||
FAIL: {
|
||||
textColor: "text-text-error-primary",
|
||||
minScore: 0,
|
||||
maxScore: 30,
|
||||
},
|
||||
WARNING: {
|
||||
textColor: "text-text-warning-primary",
|
||||
minScore: 31,
|
||||
maxScore: 60,
|
||||
},
|
||||
PASS: {
|
||||
textColor: "text-text-success-primary",
|
||||
minScore: 61,
|
||||
maxScore: 100,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const getScoreTextColor = (score: number): string => {
|
||||
for (const config of Object.values(SCORE_CONFIG)) {
|
||||
if (score >= config.minScore && score <= config.maxScore) {
|
||||
return config.textColor;
|
||||
}
|
||||
}
|
||||
|
||||
return SCORE_CONFIG.WARNING.textColor;
|
||||
};
|
||||
|
||||
export interface WatchlistItem {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
key: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface WatchlistCardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
title: string;
|
||||
items: WatchlistItem[];
|
||||
ctaLabel: string;
|
||||
ctaHref: string;
|
||||
headerAction?: React.ReactNode;
|
||||
emptyState?: {
|
||||
message?: string;
|
||||
description?: string;
|
||||
linkText?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const WatchlistCard = ({
|
||||
title,
|
||||
items,
|
||||
ctaLabel,
|
||||
ctaHref,
|
||||
headerAction,
|
||||
emptyState,
|
||||
}: WatchlistCardProps) => {
|
||||
const isEmpty = items.length === 0;
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[405px] min-w-[328px] flex-1 flex-col justify-between md:max-w-[312px]"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
{headerAction}
|
||||
</div>
|
||||
<CardContent className="flex flex-col">
|
||||
{isEmpty ? (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-12 py-6">
|
||||
{/* Icon and message */}
|
||||
<div className="flex flex-col items-center gap-6 pb-[18px]">
|
||||
<SearchX size={64} className="text-bg-data-muted" />
|
||||
<p className="text-text-neutral-tertiary w-full text-center text-sm leading-6">
|
||||
{emptyState?.message || "This space is looking empty."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description with link */}
|
||||
<p className="text-text-neutral-tertiary w-full text-sm leading-6">
|
||||
{emptyState?.description && (
|
||||
<>
|
||||
Visit the{" "}
|
||||
<Button variant="link" size="link-sm" asChild>
|
||||
<Link href={ctaHref}>
|
||||
{emptyState.linkText || ctaLabel}
|
||||
</Link>
|
||||
</Button>{" "}
|
||||
{emptyState.description}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
|
||||
// Parse numeric value if it's a percentage string (e.g., "10%")
|
||||
const numericValue =
|
||||
typeof item.value === "string"
|
||||
? parseFloat(item.value.replace("%", ""))
|
||||
: item.value;
|
||||
|
||||
// Get color based on score
|
||||
const valueColorClass = !isNaN(numericValue)
|
||||
? getScoreTextColor(numericValue)
|
||||
: "text-text-neutral-tertiary";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={cn(
|
||||
"flex h-[54px] items-center justify-between gap-2 px-3 py-[11px]",
|
||||
!isLast && "border-border-neutral-tertiary border-b",
|
||||
)}
|
||||
>
|
||||
<div className="flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white">
|
||||
{item.icon}
|
||||
</div>
|
||||
|
||||
<p className="text-text-neutral-secondary flex-1 truncate text-sm leading-6">
|
||||
{item.label}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm leading-6 font-bold",
|
||||
valueColorClass,
|
||||
)}
|
||||
>
|
||||
{item.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="mb-6">
|
||||
<Button variant="link" size="link-sm" asChild className="w-full">
|
||||
<Link href={ctaHref}>{ctaLabel}</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export function WatchlistCardSkeleton() {
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[500px] min-w-[328px] flex-col md:max-w-[312px]"
|
||||
>
|
||||
<CardTitle>
|
||||
<Skeleton className="h-7 w-[168px] rounded-xl" />
|
||||
</CardTitle>
|
||||
|
||||
<CardContent className="flex flex-1 flex-col justify-center gap-8">
|
||||
{/* 6 skeleton rows */}
|
||||
{Array.from({ length: 6 }).map((_, index) => (
|
||||
<div key={index} className="flex h-7 w-full items-start gap-6">
|
||||
<Skeleton className="h-7 w-[168px] rounded-xl" />
|
||||
<Skeleton className="h-7 flex-1 rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,11 @@ import { RiskSeverityChartSkeleton } from "./components/risk-severity-chart";
|
||||
import { RiskSeverityChartSSR } from "./components/risk-severity-chart/risk-severity-chart.ssr";
|
||||
import { StatusChartSkeleton } from "./components/status-chart";
|
||||
import { ThreatScoreSkeleton, ThreatScoreSSR } from "./components/threat-score";
|
||||
import {
|
||||
ComplianceWatchlistSSR,
|
||||
ServiceWatchlist,
|
||||
WatchlistCardSkeleton,
|
||||
} from "./components/watchlist";
|
||||
|
||||
export default async function NewOverviewPage({
|
||||
searchParams,
|
||||
@@ -50,12 +55,16 @@ export default async function NewOverviewPage({
|
||||
<RiskSeverityChartSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="mt-6 flex gap-6">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ComplianceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<FindingSeverityOverTimeSkeleton />}>
|
||||
<FindingSeverityOverTimeSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="mt-6 flex gap-6">
|
||||
<ServiceWatchlist />
|
||||
<GraphsTabsWrapper searchParams={resolvedSearchParams} />
|
||||
</div>
|
||||
</ContentLayout>
|
||||
|
||||
@@ -24,6 +24,10 @@ import {
|
||||
ThreatScoreSkeleton,
|
||||
ThreatScoreSSR,
|
||||
} from "./_new-overview/components/threat-score";
|
||||
import {
|
||||
ComplianceWatchlistSSR,
|
||||
WatchlistCardSkeleton,
|
||||
} from "./_new-overview/components/watchlist";
|
||||
|
||||
const FILTER_PREFIX = "filter[";
|
||||
|
||||
@@ -54,6 +58,10 @@ export default async function Home({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 md:flex-row md:flex-wrap md:items-stretch">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ComplianceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<ThreatScoreSkeleton />}>
|
||||
<ThreatScoreSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
@@ -108,7 +116,7 @@ const SSRDataNewFindingsTable = async ({
|
||||
|
||||
// Expand each finding with its corresponding resource, scan, and provider
|
||||
const expandedFindings = findingsData?.data
|
||||
? findingsData.data.map((finding: FindingProps) => {
|
||||
? (findingsData.data as FindingProps[]).map((finding) => {
|
||||
const scan = scanDict[finding.relationships?.scan?.data?.id];
|
||||
const resource =
|
||||
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
|
||||
@@ -148,7 +156,7 @@ const SSRDataNewFindingsTable = async ({
|
||||
<DataTable
|
||||
key={`dashboard-${Date.now()}`}
|
||||
columns={ColumnNewFindingsToDate}
|
||||
data={expandedResponse?.data || []}
|
||||
data={(expandedResponse?.data || []) as FindingProps[]}
|
||||
// metadata={findingsData?.meta}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -36,6 +36,7 @@ const buttonVariants = cva(
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
"link-sm": "text-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
import { CardVariant } from "@/components/shadcn/card/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CardVariant } from "../card";
|
||||
|
||||
export interface StatItem {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
import { Card, CardVariant } from "@/components/shadcn/card/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Card, CardVariant } from "../card";
|
||||
import type { StatItem } from "./resource-stats-card-content";
|
||||
import { ResourceStatsCardContent } from "./resource-stats-card-content";
|
||||
import { ResourceStatsCardHeader } from "./resource-stats-card-header";
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
--text-neutral-secondary: var(--color-zinc-800);
|
||||
--text-neutral-tertiary: var(--color-zinc-500);
|
||||
--text-error-primary: var(--color-red-600);
|
||||
--text-warning-primary: var(--color-orange-500);
|
||||
--text-success-primary: var(--color-green-600);
|
||||
|
||||
/* Border Colors */
|
||||
@@ -103,6 +104,7 @@
|
||||
--text-neutral-secondary: var(--color-zinc-300);
|
||||
--text-neutral-tertiary: var(--color-zinc-400);
|
||||
--text-error-primary: var(--color-red-500);
|
||||
--text-warning-primary: var(--color-orange-500);
|
||||
--text-success-primary: var(--color-green-500);
|
||||
|
||||
/* Border Colors */
|
||||
@@ -195,7 +197,9 @@
|
||||
--color-text-neutral-primary: var(--text-neutral-primary);
|
||||
--color-text-neutral-secondary: var(--text-neutral-secondary);
|
||||
--color-text-neutral-tertiary: var(--text-neutral-tertiary);
|
||||
--color-text-error: var(--text-error-primary);
|
||||
--color-text-error-primary: var(--text-error-primary);
|
||||
--color-text-warning-primary: var(--text-warning-primary);
|
||||
--color-text-success-primary: var(--text-success-primary);
|
||||
|
||||
/* Background Colors */
|
||||
--color-bg-neutral-primary: var(--bg-neutral-primary);
|
||||
|
||||
@@ -8,12 +8,20 @@ export const REQUIREMENT_STATUS = {
|
||||
export type RequirementStatus =
|
||||
(typeof REQUIREMENT_STATUS)[keyof typeof REQUIREMENT_STATUS];
|
||||
|
||||
export const COMPLIANCE_OVERVIEW_TYPE = {
|
||||
OVERVIEW: "compliance-overviews",
|
||||
REQUIREMENTS_STATUS: "compliance-requirements-status",
|
||||
} as const;
|
||||
|
||||
export type ComplianceOverviewType =
|
||||
(typeof COMPLIANCE_OVERVIEW_TYPE)[keyof typeof COMPLIANCE_OVERVIEW_TYPE];
|
||||
|
||||
export interface CompliancesOverview {
|
||||
data: ComplianceOverviewData[];
|
||||
}
|
||||
|
||||
export interface ComplianceOverviewData {
|
||||
type: "compliance-requirements-status";
|
||||
type: ComplianceOverviewType;
|
||||
id: string;
|
||||
attributes: {
|
||||
framework: string;
|
||||
|
||||
Reference in New Issue
Block a user