mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat: Check Findings component (#8976)
Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, BellOff, ShieldCheck, TriangleAlert } from "lucide-react";
|
||||
|
||||
import { DonutChart } from "@/components/graphs/donut-chart";
|
||||
import { DonutDataPoint } from "@/components/graphs/types";
|
||||
import {
|
||||
BaseCard,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
ResourceStatsCard,
|
||||
StatsContainer,
|
||||
} from "@/components/shadcn";
|
||||
import { CardVariant } from "@/components/shadcn/card/resource-stats-card/resource-stats-card-content";
|
||||
|
||||
interface CheckFindingsProps {
|
||||
failFindingsData: {
|
||||
total: number;
|
||||
new: number;
|
||||
muted: number;
|
||||
};
|
||||
passFindingsData: {
|
||||
total: number;
|
||||
new: number;
|
||||
muted: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const CheckFindings = ({
|
||||
failFindingsData,
|
||||
passFindingsData,
|
||||
}: CheckFindingsProps) => {
|
||||
// Calculate total findings
|
||||
const totalFindings = failFindingsData.total + passFindingsData.total;
|
||||
|
||||
// Calculate percentages
|
||||
const failPercentage = Math.round(
|
||||
(failFindingsData.total / totalFindings) * 100,
|
||||
);
|
||||
const passPercentage = Math.round(
|
||||
(passFindingsData.total / totalFindings) * 100,
|
||||
);
|
||||
|
||||
// Calculate change percentages (new findings as percentage change)
|
||||
const failChange =
|
||||
failFindingsData.total > 0
|
||||
? Math.round((failFindingsData.new / failFindingsData.total) * 100)
|
||||
: 0;
|
||||
const passChange =
|
||||
passFindingsData.total > 0
|
||||
? Math.round((passFindingsData.new / passFindingsData.total) * 100)
|
||||
: 0;
|
||||
|
||||
// Mock data for DonutChart
|
||||
const donutData: DonutDataPoint[] = [
|
||||
{
|
||||
name: "Fail Findings",
|
||||
value: failFindingsData.total,
|
||||
color: "#f43f5e", // Rose-500
|
||||
percentage: Number(failPercentage),
|
||||
change: Number(failChange),
|
||||
},
|
||||
{
|
||||
name: "Pass Findings",
|
||||
value: passFindingsData.total,
|
||||
color: "#4ade80", // Green-400
|
||||
percentage: Number(passPercentage),
|
||||
change: Number(passChange),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseCard>
|
||||
{/* Header */}
|
||||
<CardHeader>
|
||||
<CardTitle>Check Findings</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
{/* DonutChart Content */}
|
||||
<CardContent className="space-y-4">
|
||||
<div className="mx-auto max-h-[200px] max-w-[200px]">
|
||||
<DonutChart
|
||||
data={donutData}
|
||||
showLegend={false}
|
||||
innerRadius={66}
|
||||
outerRadius={86}
|
||||
centerLabel={{
|
||||
value: totalFindings.toLocaleString(),
|
||||
label: "Total Findings",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer with ResourceStatsCards */}
|
||||
<StatsContainer>
|
||||
<ResourceStatsCard
|
||||
containerless
|
||||
badge={{
|
||||
icon: TriangleAlert,
|
||||
count: failFindingsData.total,
|
||||
variant: CardVariant.fail,
|
||||
}}
|
||||
label="Fail Findings"
|
||||
stats={[
|
||||
{ icon: Bell, label: `${failFindingsData.new} New` },
|
||||
{ icon: BellOff, label: `${failFindingsData.muted} Muted` },
|
||||
]}
|
||||
className="flex-1"
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-center px-[46px]">
|
||||
<div className="h-full w-px bg-slate-300 dark:bg-[rgba(39,39,42,1)]" />
|
||||
</div>
|
||||
|
||||
<ResourceStatsCard
|
||||
containerless
|
||||
badge={{
|
||||
icon: ShieldCheck,
|
||||
count: passFindingsData.total,
|
||||
variant: CardVariant.pass,
|
||||
}}
|
||||
label="Pass Findings"
|
||||
stats={[
|
||||
{ icon: Bell, label: `${passFindingsData.new} New` },
|
||||
{ icon: BellOff, label: `${passFindingsData.muted} Muted` },
|
||||
]}
|
||||
className="flex-1"
|
||||
/>
|
||||
</StatsContainer>
|
||||
</CardContent>
|
||||
</BaseCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getFindingsByStatus } from "@/actions/overview/overview";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { CheckFindings } from "./components/check-findings";
|
||||
|
||||
const FILTER_PREFIX = "filter[";
|
||||
|
||||
// Extract only query params that start with "filter[" for API calls
|
||||
function pickFilterParams(
|
||||
params: SearchParamsProps | undefined | null,
|
||||
): Record<string, string | string[] | undefined> {
|
||||
if (!params) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(params).filter(([key]) => key.startsWith(FILTER_PREFIX)),
|
||||
);
|
||||
}
|
||||
|
||||
export default async function NewOverviewPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParamsProps>;
|
||||
}) {
|
||||
const resolvedSearchParams = await searchParams;
|
||||
|
||||
return (
|
||||
<ContentLayout title="New Overview" icon="lucide:square-chart-gantt">
|
||||
<div className="flex min-h-[60vh] items-center justify-center p-6">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-[400px] w-full max-w-md items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Loading...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SSRCheckFindings searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const SSRCheckFindings = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps | undefined | null;
|
||||
}) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const findingsByStatus = await getFindingsByStatus({ filters });
|
||||
|
||||
if (!findingsByStatus) {
|
||||
return (
|
||||
<div className="flex h-[400px] w-full max-w-md items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Failed to load findings data</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
fail = 0,
|
||||
pass = 0,
|
||||
muted_new = 0,
|
||||
muted_changed = 0,
|
||||
fail_new = 0,
|
||||
pass_new = 0,
|
||||
} = findingsByStatus?.data?.attributes || {};
|
||||
|
||||
const mutedTotal = muted_new + muted_changed;
|
||||
|
||||
return (
|
||||
<CheckFindings
|
||||
failFindingsData={{
|
||||
total: fail,
|
||||
new: fail_new,
|
||||
muted: mutedTotal,
|
||||
}}
|
||||
passFindingsData={{
|
||||
total: pass,
|
||||
new: pass_new,
|
||||
muted: mutedTotal,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -21,44 +21,39 @@ interface DonutChartProps {
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
backgroundColor: "var(--chart-background)",
|
||||
borderColor: "var(--chart-border-emphasis)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: data.color }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
>
|
||||
{data.percentage}% {data.name}
|
||||
</span>
|
||||
</div>
|
||||
{data.change !== undefined && (
|
||||
<p
|
||||
className="mt-2 text-xs"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{data.change > 0 ? "+" : ""}
|
||||
{data.change}%
|
||||
</span>{" "}
|
||||
Since last scan
|
||||
</p>
|
||||
)}
|
||||
if (!active || !payload || !payload.length) return null;
|
||||
|
||||
const entry = payload[0];
|
||||
const name = entry.name;
|
||||
const percentage = entry.payload?.percentage;
|
||||
const color = entry.color || entry.payload?.color;
|
||||
const change = entry.payload?.change;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-3 shadow-lg dark:border-slate-600 dark:bg-slate-800">
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className="h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="text-sm font-semibold text-slate-600 dark:text-zinc-300">
|
||||
{percentage}%
|
||||
</span>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
<p className="mt-1 text-xs text-slate-600 dark:text-zinc-300">
|
||||
{change !== undefined && (
|
||||
<>
|
||||
<span className="font-bold">
|
||||
{change > 0 ? "+" : ""}
|
||||
{change}%
|
||||
</span>
|
||||
<span> Since Last Scan</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomLegend = ({ payload }: any) => {
|
||||
@@ -72,8 +67,8 @@ const CustomLegend = ({ payload }: any) => {
|
||||
|
||||
export function DonutChart({
|
||||
data,
|
||||
innerRadius = 80,
|
||||
outerRadius = 120,
|
||||
innerRadius = 68,
|
||||
outerRadius = 86,
|
||||
showLegend = true,
|
||||
centerLabel,
|
||||
}: DonutChartProps) {
|
||||
@@ -108,7 +103,7 @@ export function DonutChart({
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square max-h-[350px]"
|
||||
@@ -122,7 +117,7 @@ export function DonutChart({
|
||||
innerRadius={innerRadius}
|
||||
outerRadius={outerRadius}
|
||||
strokeWidth={0}
|
||||
paddingAngle={2}
|
||||
paddingAngle={0}
|
||||
>
|
||||
{chartData.map((entry, index) => {
|
||||
const opacity =
|
||||
@@ -157,9 +152,9 @@ export function DonutChart({
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="text-3xl font-bold"
|
||||
className="text-3xl font-bold text-black dark:text-white"
|
||||
style={{
|
||||
fill: "var(--chart-text-primary)",
|
||||
fill: "currentColor",
|
||||
}}
|
||||
>
|
||||
{formattedValue}
|
||||
@@ -167,8 +162,9 @@ export function DonutChart({
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="text-black dark:text-white"
|
||||
style={{
|
||||
fill: "var(--chart-text-secondary)",
|
||||
fill: "currentColor",
|
||||
}}
|
||||
>
|
||||
{centerLabel.label}
|
||||
@@ -183,6 +179,6 @@ export function DonutChart({
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
{showLegend && <CustomLegend payload={legendPayload} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ChartTooltip({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-w-[200px] rounded-lg border p-3 shadow-lg"
|
||||
className="min-w-[200px] rounded-lg border border-slate-200 bg-white p-3 shadow-lg dark:border-slate-600 dark:bg-slate-800"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
@@ -45,15 +45,12 @@ export function ChartTooltip({
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
)}
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
<p className="text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{label || data.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-xs" style={{ color: CHART_COLORS.textPrimary }}>
|
||||
<p className="mt-1 text-xs text-slate-900 dark:text-white">
|
||||
{typeof data.value === "number"
|
||||
? data.value.toLocaleString()
|
||||
: data.value}
|
||||
@@ -62,11 +59,8 @@ export function ChartTooltip({
|
||||
|
||||
{data.newFindings !== undefined && data.newFindings > 0 && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<Bell size={14} className="text-slate-600 dark:text-slate-400" />
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{data.newFindings} New Findings
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,11 +68,8 @@ export function ChartTooltip({
|
||||
|
||||
{data.new !== undefined && data.new > 0 && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<Bell size={14} className="text-slate-600 dark:text-slate-400" />
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{data.new} New
|
||||
</span>
|
||||
</div>
|
||||
@@ -86,21 +77,15 @@ export function ChartTooltip({
|
||||
|
||||
{data.muted !== undefined && data.muted > 0 && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<VolumeX size={14} style={{ color: CHART_COLORS.textSecondary }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<VolumeX size={14} className="text-slate-600 dark:text-slate-400" />
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{data.muted} Muted
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.change !== undefined && (
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<p className="mt-1 text-xs text-slate-600 dark:text-slate-400">
|
||||
<span className="font-bold">
|
||||
{data.change > 0 ? "+" : ""}
|
||||
{data.change}%
|
||||
@@ -125,17 +110,8 @@ export function MultiSeriesChartTooltip({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-w-[200px] rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="mb-2 text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
<div className="min-w-[200px] rounded-lg border border-slate-200 bg-white p-3 shadow-lg dark:border-slate-600 dark:bg-slate-800">
|
||||
<p className="mb-2 text-sm font-semibold text-slate-900 dark:text-white">
|
||||
{label}
|
||||
</p>
|
||||
|
||||
@@ -145,20 +121,14 @@ export function MultiSeriesChartTooltip({
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-xs" style={{ color: CHART_COLORS.textPrimary }}>
|
||||
<span className="text-xs text-slate-900 dark:text-white">
|
||||
{entry.name}:
|
||||
</span>
|
||||
<span
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
<span className="text-xs font-semibold text-slate-900 dark:text-white">
|
||||
{entry.value}
|
||||
</span>
|
||||
{entry.payload[`${entry.dataKey}_change`] && (
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<span className="text-xs text-slate-600 dark:text-slate-400">
|
||||
({entry.payload[`${entry.dataKey}_change`] > 0 ? "+" : ""}
|
||||
{entry.payload[`${entry.dataKey}_change`]}%)
|
||||
</span>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const title = tv({
|
||||
base: "tracking-tight inline font-semibold",
|
||||
variants: {
|
||||
color: {
|
||||
violet: "from-[#FF1CF7] to-[#b249f8]",
|
||||
yellow: "from-[#FF705B] to-[#FFB457]",
|
||||
blue: "from-[#5EA2EF] to-[#0072F5]",
|
||||
cyan: "from-[#00b7fa] to-[#01cfea]",
|
||||
green: "from-[#6FEE8D] to-[#17c964]",
|
||||
pink: "from-[#FF72E1] to-[#F54C7A]",
|
||||
foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
|
||||
},
|
||||
size: {
|
||||
sm: "text-3xl lg:text-4xl",
|
||||
md: "text-[2.3rem] lg:text-5xl leading-9",
|
||||
lg: "text-4xl lg:text-6xl",
|
||||
},
|
||||
fullWidth: {
|
||||
true: "w-full block",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
color: [
|
||||
"violet",
|
||||
"yellow",
|
||||
"blue",
|
||||
"cyan",
|
||||
"green",
|
||||
"pink",
|
||||
"foreground",
|
||||
],
|
||||
class: "bg-clip-text text-transparent bg-linear-to-b",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const subtitle = tv({
|
||||
base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
|
||||
variants: {
|
||||
fullWidth: {
|
||||
true: "w-full!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
fullWidth: true,
|
||||
},
|
||||
});
|
||||
@@ -4,13 +4,18 @@ This directory contains all shadcn/ui based components for the Prowler applicati
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Example of a custom component:
|
||||
|
||||
```
|
||||
shadcn/
|
||||
├── card.tsx # shadcn Card component
|
||||
├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn
|
||||
│ ├── resource-stats-card.tsx
|
||||
│ ├── resource-stats-card.example.tsx
|
||||
│ └── index.ts
|
||||
├── card/
|
||||
│ ├── base-card/
|
||||
│ │ ├── base-card.tsx
|
||||
│ ├── card/
|
||||
│ │ ├── card.tsx
|
||||
│ └── resource-stats-card/
|
||||
│ ├── resource-stats-card.tsx
|
||||
│ ├── resource-stats-card.example.tsx
|
||||
├── index.ts # Barrel exports
|
||||
└── README.md
|
||||
```
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Card } from "../card";
|
||||
|
||||
const baseCardVariants = cva("", {
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-slate-200 bg-white dark:border-zinc-900 dark:bg-stone-950",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface BaseCardProps
|
||||
extends React.ComponentProps<typeof Card>,
|
||||
VariantProps<typeof baseCardVariants> {}
|
||||
|
||||
const BaseCard = ({ className, variant, ...props }: BaseCardProps) => {
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
baseCardVariants({ variant }),
|
||||
"gap-2 px-[18px] pt-3 pb-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { BaseCard };
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
@@ -20,7 +18,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -32,7 +30,10 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn(
|
||||
"my-2 text-[18px] leading-none text-slate-900 dark:text-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
+8
-8
@@ -33,11 +33,11 @@ const badgeVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
[CardVariant.default]: "bg-[#535359]",
|
||||
[CardVariant.fail]: "bg-[#432232]",
|
||||
[CardVariant.pass]: "bg-[#204237]",
|
||||
[CardVariant.warning]: "bg-[#3d3520]",
|
||||
[CardVariant.info]: "bg-[#1e3a5f]",
|
||||
[CardVariant.default]: "bg-slate-100 dark:bg-[#535359]",
|
||||
[CardVariant.fail]: "bg-red-100 dark:bg-[#432232]",
|
||||
[CardVariant.pass]: "bg-green-100 dark:bg-[#204237]",
|
||||
[CardVariant.warning]: "bg-amber-100 dark:bg-[#3d3520]",
|
||||
[CardVariant.info]: "bg-blue-100 dark:bg-[#1e3a5f]",
|
||||
},
|
||||
size: {
|
||||
sm: "px-1 text-xs",
|
||||
@@ -66,7 +66,7 @@ const badgeIconVariants = cva("", {
|
||||
});
|
||||
|
||||
const labelTextVariants = cva(
|
||||
"leading-6 font-semibold text-zinc-300 dark:text-zinc-300",
|
||||
"leading-6 font-semibold text-slate-900 dark:text-zinc-300 whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
@@ -81,7 +81,7 @@ const labelTextVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const statIconVariants = cva("text-zinc-300 dark:text-zinc-300", {
|
||||
const statIconVariants = cva("text-slate-600 dark:text-zinc-300", {
|
||||
variants: {
|
||||
size: {
|
||||
sm: "h-2.5 w-2.5",
|
||||
@@ -95,7 +95,7 @@ const statIconVariants = cva("text-zinc-300 dark:text-zinc-300", {
|
||||
});
|
||||
|
||||
const statLabelVariants = cva(
|
||||
"leading-5 font-medium text-zinc-300 dark:text-zinc-300",
|
||||
"leading-5 font-medium text-slate-700 dark:text-zinc-300",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
+2
-2
@@ -111,7 +111,7 @@ export const ResourceStatsCard = ({
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<p className="text-center text-sm leading-5 font-medium text-zinc-300 dark:text-zinc-300">
|
||||
<p className="text-center text-sm leading-5 font-medium text-slate-600 dark:text-zinc-300">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
</div>
|
||||
@@ -141,7 +141,7 @@ export const ResourceStatsCard = ({
|
||||
{header && <ResourceStatsCardHeader {...header} size={resolvedSize} />}
|
||||
{emptyState ? (
|
||||
<div className="flex h-[51px] w-full flex-col items-center justify-center">
|
||||
<p className="text-center text-sm leading-5 font-medium text-zinc-300 dark:text-zinc-300">
|
||||
<p className="text-center text-sm leading-5 font-medium text-slate-600 dark:text-zinc-300">
|
||||
{emptyState.message}
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface StatsContainerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const StatsContainer = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: StatsContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex rounded-xl border border-slate-200 bg-white px-[19px] py-[9px] dark:border-[rgba(38,38,38,0.7)] dark:bg-[rgba(23,23,23,0.5)] dark:backdrop-blur-[46px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatsContainer };
|
||||
@@ -1,21 +1,8 @@
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./card";
|
||||
export {
|
||||
ResourceStatsCard,
|
||||
ResourceStatsCardContainer,
|
||||
type ResourceStatsCardContainerProps,
|
||||
ResourceStatsCardContent,
|
||||
type ResourceStatsCardContentProps,
|
||||
ResourceStatsCardDivider,
|
||||
type ResourceStatsCardDividerProps,
|
||||
ResourceStatsCardHeader,
|
||||
type ResourceStatsCardHeaderProps,
|
||||
type ResourceStatsCardProps,
|
||||
type StatItem,
|
||||
} from "./resource-stats-card";
|
||||
export * from "./card/base-card/base-card";
|
||||
export * from "./card/card";
|
||||
export * from "./card/resource-stats-card/resource-stats-card";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-container";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-content";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-divider";
|
||||
export * from "./card/resource-stats-card/resource-stats-card-header";
|
||||
export * from "./card/stats-container";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export type { ResourceStatsCardProps } from "./resource-stats-card";
|
||||
export { ResourceStatsCard } from "./resource-stats-card";
|
||||
export type { ResourceStatsCardContainerProps } from "./resource-stats-card-container";
|
||||
export { ResourceStatsCardContainer } from "./resource-stats-card-container";
|
||||
export type {
|
||||
ResourceStatsCardContentProps,
|
||||
StatItem,
|
||||
} from "./resource-stats-card-content";
|
||||
export { ResourceStatsCardContent } from "./resource-stats-card-content";
|
||||
export type { ResourceStatsCardDividerProps } from "./resource-stats-card-divider";
|
||||
export { ResourceStatsCardDivider } from "./resource-stats-card-divider";
|
||||
export type { ResourceStatsCardHeaderProps } from "./resource-stats-card-header";
|
||||
export { ResourceStatsCardHeader } from "./resource-stats-card-header";
|
||||
@@ -70,6 +70,7 @@ const ChartContainer = React.forwardRef<
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
ChartContainer.displayName = "Chart";
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
@@ -184,7 +185,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-32 items-start gap-1.5 rounded-lg border border-slate-200 border-slate-200/50 bg-white px-2.5 py-1.5 text-xs shadow-xl dark:border-slate-800 dark:border-slate-800/50 dark:bg-slate-950",
|
||||
"grid min-w-32 items-start gap-1.5 rounded-lg border border-slate-200/50 bg-white px-2.5 py-1.5 text-xs shadow-xl dark:border-slate-800/50 dark:bg-slate-950",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -86,7 +86,7 @@ export const CustomButton = React.forwardRef<
|
||||
) => (
|
||||
<Button
|
||||
as={asLink ? Link : undefined}
|
||||
href={asLink}
|
||||
{...(asLink && { href: asLink })}
|
||||
target={target}
|
||||
type={type}
|
||||
aria-label={ariaLabel}
|
||||
|
||||
Reference in New Issue
Block a user