mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat: add risk severity chart to new overview page (#9041)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart";
|
||||
import { BarDataPoint } from "@/components/graphs/types";
|
||||
import {
|
||||
BaseCard,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
interface RiskSeverityChartProps {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
informational: number;
|
||||
}
|
||||
|
||||
export const RiskSeverityChart = ({
|
||||
critical,
|
||||
high,
|
||||
medium,
|
||||
low,
|
||||
informational,
|
||||
}: RiskSeverityChartProps) => {
|
||||
// Calculate total findings
|
||||
const totalFindings = critical + high + medium + low + informational;
|
||||
|
||||
// Transform data to BarDataPoint format
|
||||
const chartData: BarDataPoint[] = [
|
||||
{
|
||||
name: "Critical",
|
||||
value: critical,
|
||||
percentage: calculatePercentage(critical, totalFindings),
|
||||
},
|
||||
{
|
||||
name: "High",
|
||||
value: high,
|
||||
percentage: calculatePercentage(high, totalFindings),
|
||||
},
|
||||
{
|
||||
name: "Medium",
|
||||
value: medium,
|
||||
percentage: calculatePercentage(medium, totalFindings),
|
||||
},
|
||||
{
|
||||
name: "Low",
|
||||
value: low,
|
||||
percentage: calculatePercentage(low, totalFindings),
|
||||
},
|
||||
{
|
||||
name: "Info",
|
||||
value: informational,
|
||||
percentage: calculatePercentage(informational, totalFindings),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseCard className="flex h-full flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>Risk Severity</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 items-center justify-start px-6">
|
||||
<HorizontalBarChart data={chartData} />
|
||||
</CardContent>
|
||||
</BaseCard>
|
||||
);
|
||||
};
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
ResourceStatsCard,
|
||||
ResourceStatsCardContainer,
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
interface CheckFindingsProps {
|
||||
interface StatusChartProps {
|
||||
failFindingsData: {
|
||||
total: number;
|
||||
new: number;
|
||||
@@ -27,30 +28,32 @@ interface CheckFindingsProps {
|
||||
};
|
||||
}
|
||||
|
||||
export const CheckFindings = ({
|
||||
export const StatusChart = ({
|
||||
failFindingsData,
|
||||
passFindingsData,
|
||||
}: CheckFindingsProps) => {
|
||||
}: StatusChartProps) => {
|
||||
// Calculate total findings
|
||||
const totalFindings = failFindingsData.total + passFindingsData.total;
|
||||
|
||||
// Calculate percentages
|
||||
const failPercentage = Math.round(
|
||||
(failFindingsData.total / totalFindings) * 100,
|
||||
const failPercentage = calculatePercentage(
|
||||
failFindingsData.total,
|
||||
totalFindings,
|
||||
);
|
||||
const passPercentage = Math.round(
|
||||
(passFindingsData.total / totalFindings) * 100,
|
||||
const passPercentage = calculatePercentage(
|
||||
passFindingsData.total,
|
||||
totalFindings,
|
||||
);
|
||||
|
||||
// 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;
|
||||
const failChange = calculatePercentage(
|
||||
failFindingsData.new,
|
||||
failFindingsData.total,
|
||||
);
|
||||
const passChange = calculatePercentage(
|
||||
passFindingsData.new,
|
||||
passFindingsData.total,
|
||||
);
|
||||
|
||||
// Mock data for DonutChart
|
||||
const donutData: DonutDataPoint[] = [
|
||||
@@ -72,13 +75,11 @@ export const CheckFindings = ({
|
||||
|
||||
return (
|
||||
<BaseCard>
|
||||
{/* Header */}
|
||||
<CardHeader>
|
||||
<CardTitle>Check Findings</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
{/* DonutChart Content */}
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-2">
|
||||
<div className="mx-auto max-h-[200px] max-w-[200px]">
|
||||
<DonutChart
|
||||
data={donutData}
|
||||
@@ -92,8 +93,7 @@ export const CheckFindings = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer with ResourceStatsCards */}
|
||||
<ResourceStatsCardContainer className="flex w-full flex-col items-start justify-center gap-4 sm:flex-row md:w-[480px] md:justify-between">
|
||||
<ResourceStatsCardContainer className="flex w-full flex-col items-start justify-center gap-4 lg:flex-row lg:justify-between">
|
||||
<ResourceStatsCard
|
||||
containerless
|
||||
badge={{
|
||||
@@ -111,11 +111,11 @@ export const CheckFindings = ({
|
||||
? { message: "No failed findings to display" }
|
||||
: undefined
|
||||
}
|
||||
className="flex-1"
|
||||
className="w-full lg:min-w-0 lg:flex-1"
|
||||
/>
|
||||
|
||||
<div className="flex w-full items-center justify-center sm:w-auto sm:self-stretch sm:px-[46px]">
|
||||
<div className="h-px w-full bg-slate-300 sm:h-full sm:w-px dark:bg-[rgba(39,39,42,1)]" />
|
||||
<div className="flex w-full items-center justify-center lg:w-auto lg:self-stretch">
|
||||
<div className="h-px w-full bg-slate-300 lg:h-full lg:w-px dark:bg-[rgba(39,39,42,1)]" />
|
||||
</div>
|
||||
|
||||
<ResourceStatsCard
|
||||
@@ -135,7 +135,7 @@ export const CheckFindings = ({
|
||||
? { message: "No passed findings to display" }
|
||||
: undefined
|
||||
}
|
||||
className="flex-1"
|
||||
className="w-full lg:min-w-0 lg:flex-1"
|
||||
/>
|
||||
</ResourceStatsCardContainer>
|
||||
</CardContent>
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getFindingsByStatus } from "@/actions/overview/overview";
|
||||
import {
|
||||
getFindingsBySeverity,
|
||||
getFindingsByStatus,
|
||||
} from "@/actions/overview/overview";
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { AccountsSelector } from "./components/accounts-selector";
|
||||
import { CheckFindings } from "./components/check-findings";
|
||||
import { ProviderTypeSelector } from "./components/provider-type-selector";
|
||||
import { RiskSeverityChart } from "./components/risk-severity-chart";
|
||||
import { StatusChart } from "./components/status-chart";
|
||||
|
||||
const FILTER_PREFIX = "filter[";
|
||||
|
||||
@@ -35,16 +39,26 @@ export default async function NewOverviewPage({
|
||||
<ProviderTypeSelector providers={providersData?.data ?? []} />
|
||||
<AccountsSelector providers={providersData?.data ?? []} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 md:flex-row">
|
||||
<div className="grid auto-rows-fr gap-6 md:grid-cols-2">
|
||||
<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">
|
||||
<div className="flex h-[400px] w-full 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>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Loading...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SSRRiskSeverityChart searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</ContentLayout>
|
||||
);
|
||||
@@ -79,7 +93,7 @@ const SSRCheckFindings = async ({
|
||||
const mutedTotal = muted_new + muted_changed;
|
||||
|
||||
return (
|
||||
<CheckFindings
|
||||
<StatusChart
|
||||
failFindingsData={{
|
||||
total: fail,
|
||||
new: fail_new,
|
||||
@@ -93,3 +107,39 @@ const SSRCheckFindings = async ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SSRRiskSeverityChart = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps | undefined | null;
|
||||
}) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const findingsBySeverity = await getFindingsBySeverity({ filters });
|
||||
|
||||
if (!findingsBySeverity) {
|
||||
return (
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-xl border border-zinc-900 bg-stone-950">
|
||||
<p className="text-zinc-400">Failed to load severity data</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
critical = 0,
|
||||
high = 0,
|
||||
medium = 0,
|
||||
low = 0,
|
||||
informational = 0,
|
||||
} = findingsBySeverity?.data?.attributes || {};
|
||||
|
||||
return (
|
||||
<RiskSeverityChart
|
||||
critical={critical}
|
||||
high={high}
|
||||
medium={medium}
|
||||
low={low}
|
||||
informational={informational}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,28 +30,29 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
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>
|
||||
<p className="mt-1 text-xs text-slate-600 dark:text-zinc-300">
|
||||
<div className="rounded-xl border border-slate-200 bg-white px-3 py-1.5 shadow-lg dark:border-[#202020] dark:bg-[#121110]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{/* Title with color chip */}
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className="size-3 shrink-0 rounded"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<p className="text-sm leading-5 font-medium text-slate-900 dark:text-[#f4f4f5]">
|
||||
{percentage}% {name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Change percentage row */}
|
||||
{change !== undefined && (
|
||||
<>
|
||||
<span className="font-bold">
|
||||
<div className="flex items-start">
|
||||
<p className="text-sm leading-5 font-medium text-slate-600 dark:text-[#d4d4d8]">
|
||||
{change > 0 ? "+" : ""}
|
||||
{change}%
|
||||
</span>
|
||||
<span> Since Last Scan</span>
|
||||
</>
|
||||
{change}% Since last scan
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,16 +16,26 @@ interface HorizontalBarChartProps {
|
||||
export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
const total = data.reduce((sum, d) => sum + (Number(d.value) || 0), 0);
|
||||
const isEmpty = total <= 0;
|
||||
|
||||
const emptyData: BarDataPoint[] = [
|
||||
{ name: "Critical", value: 1, percentage: 100 },
|
||||
{ name: "High", value: 1, percentage: 100 },
|
||||
{ name: "Medium", value: 1, percentage: 100 },
|
||||
{ name: "Low", value: 1, percentage: 100 },
|
||||
];
|
||||
|
||||
const sortedData = (isEmpty ? emptyData : [...data]).sort((a, b) => {
|
||||
const orderA = SEVERITY_ORDER[a.name as keyof typeof SEVERITY_ORDER] ?? 999;
|
||||
const orderB = SEVERITY_ORDER[b.name as keyof typeof SEVERITY_ORDER] ?? 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full space-y-6">
|
||||
{title && (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<h3
|
||||
className="text-lg font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
@@ -37,23 +47,25 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
|
||||
<div className="space-y-6">
|
||||
{sortedData.map((item, index) => {
|
||||
const isHovered = hoveredIndex === index;
|
||||
const isFaded = hoveredIndex !== null && !isHovered;
|
||||
const barColor =
|
||||
item.color ||
|
||||
getSeverityColorByName(item.name) ||
|
||||
CHART_COLORS.defaultColor;
|
||||
const isHovered = !isEmpty && hoveredIndex === index;
|
||||
const isFaded = !isEmpty && hoveredIndex !== null && !isHovered;
|
||||
const barColor = isEmpty
|
||||
? CHART_COLORS.gridLine
|
||||
: item.color ||
|
||||
getSeverityColorByName(item.name) ||
|
||||
CHART_COLORS.defaultColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="relative flex items-center gap-4"
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
className="flex items-center gap-6"
|
||||
onMouseEnter={() => !isEmpty && setHoveredIndex(index)}
|
||||
onMouseLeave={() => !isEmpty && setHoveredIndex(null)}
|
||||
>
|
||||
<div className="w-24 text-right">
|
||||
{/* Label */}
|
||||
<div className="w-20 shrink-0">
|
||||
<span
|
||||
className="text-sm"
|
||||
className="text-sm font-medium"
|
||||
style={{
|
||||
color: "var(--chart-text-primary)",
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
@@ -64,78 +76,84 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar - flexible */}
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute inset-0 h-8 w-full rounded-lg bg-slate-700/50" />
|
||||
<div
|
||||
className="relative h-8 rounded-lg transition-all duration-300"
|
||||
style={{
|
||||
width: `${item.percentage || (item.value / Math.max(...data.map((d) => d.value))) * 100}%`,
|
||||
backgroundColor: barColor,
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 h-[22px] w-full rounded-xl bg-[#FAFAFA] dark:bg-black" />
|
||||
{(item.value > 0 || isEmpty) && (
|
||||
<div
|
||||
className="relative h-[22px] rounded-[4px] border border-black/10 transition-all duration-300"
|
||||
style={{
|
||||
width: isEmpty
|
||||
? `${item.percentage}%`
|
||||
: `${item.percentage || (item.value / Math.max(...data.map((d) => d.value))) * 100}%`,
|
||||
backgroundColor: barColor,
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHovered && (
|
||||
<div
|
||||
className="absolute top-10 left-0 z-10 min-w-[200px] 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: barColor }}
|
||||
/>
|
||||
<span
|
||||
className="font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
>
|
||||
{item.value.toLocaleString()} {item.name} Risk
|
||||
</span>
|
||||
</div>
|
||||
{item.newFindings !== undefined && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Bell
|
||||
size={14}
|
||||
style={{ color: "var(--chart-fail)" }}
|
||||
<div className="absolute top-10 left-0 z-10 rounded-xl border border-slate-200 bg-white px-3 py-1.5 shadow-lg dark:border-[#202020] dark:bg-[#121110]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{/* Title with color chip */}
|
||||
<div className="flex items-center gap-1">
|
||||
<div
|
||||
className="size-3 shrink-0 rounded"
|
||||
style={{ backgroundColor: barColor }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
{item.newFindings} New Findings
|
||||
</span>
|
||||
<p className="text-sm leading-5 font-medium text-slate-900 dark:text-[#f4f4f5]">
|
||||
{item.value.toLocaleString()} {item.name} Risk
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{item.change !== undefined && (
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{item.change > 0 ? "+" : ""}
|
||||
{item.change}%
|
||||
</span>{" "}
|
||||
Since Last Scan
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* New Findings row */}
|
||||
{item.newFindings !== undefined && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Bell
|
||||
size={12}
|
||||
className="shrink-0 text-slate-600 dark:text-[#d4d4d8]"
|
||||
/>
|
||||
<p className="text-sm leading-5 font-medium text-slate-600 dark:text-[#d4d4d8]">
|
||||
{item.newFindings} New Findings
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Change percentage row */}
|
||||
{item.change !== undefined && (
|
||||
<div className="flex items-start">
|
||||
<p className="text-sm leading-5 font-medium text-slate-600 dark:text-[#d4d4d8]">
|
||||
{item.change > 0 ? "+" : ""}
|
||||
{item.change}% Since last scan
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Percentage and Count */}
|
||||
<div
|
||||
className="flex w-40 items-center gap-2 text-sm"
|
||||
className="flex w-[90px] shrink-0 items-center gap-2 text-sm"
|
||||
style={{
|
||||
color: "var(--chart-text-primary)",
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
>
|
||||
<span className="font-semibold">{item.percentage}%</span>
|
||||
<span style={{ color: "var(--chart-text-secondary)" }}>•</span>
|
||||
<span className="font-bold">{item.value.toLocaleString()}</span>
|
||||
<span className="w-[26px] text-right font-medium">
|
||||
{isEmpty ? "0" : item.percentage}%
|
||||
</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
•
|
||||
</span>
|
||||
<span className="font-bold">
|
||||
{isEmpty ? "0" : item.value.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,3 +6,14 @@ export function cn(...inputs: ClassValue[]) {
|
||||
}
|
||||
|
||||
export const SPECIAL_CHARACTERS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
|
||||
|
||||
/**
|
||||
* Calculates a percentage and rounds it to the nearest integer
|
||||
* @param value - The numerator value
|
||||
* @param total - The denominator value
|
||||
* @returns The rounded percentage (0-100), or 0 if total is 0
|
||||
*/
|
||||
export function calculatePercentage(value: number, total: number): number {
|
||||
if (total === 0) return 0;
|
||||
return Math.round((value / total) * 100);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
@theme {
|
||||
/* Chart Severity Colors - Dark Theme */
|
||||
--chart-info: #2e51b2;
|
||||
--chart-warning: #fdd34f;
|
||||
--chart-warning-emphasis: #ff7d19;
|
||||
--chart-danger: #ff3077;
|
||||
--chart-danger-emphasis: #971348;
|
||||
--chart-info: #3C8DFF;
|
||||
--chart-warning: #FDFBD4;
|
||||
--chart-warning-emphasis: #FEC94D;
|
||||
--chart-danger: #F77852;
|
||||
--chart-danger-emphasis: #FF006A;
|
||||
|
||||
/* Chart Status Colors */
|
||||
--chart-success-color: #86da26;
|
||||
@@ -37,11 +37,11 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Light Theme Chart Colors */
|
||||
--chart-info: #1e40af;
|
||||
--chart-warning: #d97706;
|
||||
--chart-warning-emphasis: #dc2626;
|
||||
--chart-danger: #dc2626;
|
||||
--chart-danger-emphasis: #991b1b;
|
||||
--chart-info: #3C8DFF;
|
||||
--chart-warning: #FDFBD4;
|
||||
--chart-warning-emphasis: #FEC94D;
|
||||
--chart-danger: #F77852;
|
||||
--chart-danger-emphasis: #FF006A;
|
||||
--chart-success-color: #16a34a;
|
||||
--chart-fail: #dc2626;
|
||||
--chart-radar-primary: #9d174d;
|
||||
@@ -69,11 +69,11 @@
|
||||
|
||||
.dark {
|
||||
/* Dark Theme Chart Colors */
|
||||
--chart-info: #2e51b2;
|
||||
--chart-warning: #fdd34f;
|
||||
--chart-warning-emphasis: #ff7d19;
|
||||
--chart-danger: #ff3077;
|
||||
--chart-danger-emphasis: #971348;
|
||||
--chart-info: #3C8DFF;
|
||||
--chart-warning: #FDFBD4;
|
||||
--chart-warning-emphasis: #FEC94D;
|
||||
--chart-danger: #F77852;
|
||||
--chart-danger-emphasis: #FF006A;
|
||||
--chart-success-color: #86da26;
|
||||
--chart-fail: #db2b49;
|
||||
--chart-radar-primary: #b51c80;
|
||||
|
||||
Reference in New Issue
Block a user