refactor(graphs): graph components kebab case (#8966)

Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
Alan Buscaglia
2025-10-22 15:51:43 +02:00
committed by GitHub
parent f596907223
commit b4ff1dcc75
24 changed files with 2899 additions and 788 deletions

View File

@@ -46,6 +46,14 @@ help: ## Show this help.
@echo "Prowler Makefile"
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Build no cache
build-no-cache-dev:
docker compose -f docker-compose-dev.yml build --no-cache api-dev worker-dev worker-beat
##@ Development Environment
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, and workers
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat --build
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat
##@ Development Environment
build-and-run-api-dev: build-no-cache-dev run-api-dev

View File

@@ -1,162 +0,0 @@
"use client";
import {
Bar,
BarChart as RechartsBar,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { ChartTooltip } from "./shared/ChartTooltip";
import { CHART_COLORS, LAYOUT_OPTIONS } from "./shared/constants";
import { getSeverityColorByName } from "./shared/utils";
import { BarDataPoint, LayoutOption } from "./types";
interface BarChartProps {
data: BarDataPoint[];
layout?: LayoutOption;
xLabel?: string;
yLabel?: string;
height?: number;
showValues?: boolean;
}
const CustomLabel = ({ x, y, width, height, value, data }: any) => {
const percentage = data.percentage;
return (
<text
x={x + width + 10}
y={y + height / 2}
fill={CHART_COLORS.textSecondary}
fontSize={12}
textAnchor="start"
dominantBaseline="middle"
>
{percentage !== undefined
? `${percentage}% • ${value.toLocaleString()}`
: value.toLocaleString()}
</text>
);
};
export function BarChart({
data,
layout = LAYOUT_OPTIONS.horizontal,
xLabel,
yLabel,
height = 400,
showValues = true,
}: BarChartProps) {
const isHorizontal = layout === LAYOUT_OPTIONS.horizontal;
return (
<ResponsiveContainer width="100%" height={height}>
<RechartsBar
data={data}
layout={layout}
margin={{ top: 20, right: showValues ? 100 : 30, left: 20, bottom: 20 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke={CHART_COLORS.gridLine}
horizontal={isHorizontal}
vertical={!isHorizontal}
/>
{isHorizontal ? (
<>
<XAxis
type="number"
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
label={
xLabel
? {
value: xLabel,
position: "insideBottom",
offset: -10,
fill: CHART_COLORS.textSecondary,
}
: undefined
}
/>
<YAxis
dataKey="name"
type="category"
width={100}
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
label={
yLabel
? {
value: yLabel,
angle: -90,
position: "insideLeft",
fill: CHART_COLORS.textSecondary,
}
: undefined
}
/>
</>
) : (
<>
<XAxis
dataKey="name"
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
label={
xLabel
? {
value: xLabel,
position: "insideBottom",
offset: -10,
fill: CHART_COLORS.textSecondary,
}
: undefined
}
/>
<YAxis
type="number"
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
label={
yLabel
? {
value: yLabel,
angle: -90,
position: "insideLeft",
fill: CHART_COLORS.textSecondary,
}
: undefined
}
/>
</>
)}
<Tooltip content={<ChartTooltip />} />
<Bar
dataKey="value"
radius={4}
label={
showValues && isHorizontal
? (props: any) => (
<CustomLabel {...props} data={data[props.index]} />
)
: false
}
>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={
entry.color ||
getSeverityColorByName(entry.name) ||
CHART_COLORS.defaultColor
}
opacity={1}
className="transition-opacity hover:opacity-80"
/>
))}
</Bar>
</RechartsBar>
</ResponsiveContainer>
);
}

View File

@@ -1,137 +0,0 @@
"use client";
import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts";
import { CHART_COLORS, SEVERITY_COLORS } from "./shared/constants";
interface SankeyNode {
name: string;
}
interface SankeyLink {
source: number;
target: number;
value: number;
}
interface SankeyChartProps {
data: {
nodes: SankeyNode[];
links: SankeyLink[];
};
height?: number;
}
const COLORS: Record<string, string> = {
Success: "var(--color-success)",
Fail: "var(--color-destructive)",
AWS: "var(--color-orange)",
Azure: "var(--color-cyan)",
Google: "var(--color-red)",
...SEVERITY_COLORS,
};
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="text-sm font-semibold text-white">{data.name}</p>
{data.value && (
<p className="text-xs text-slate-400">Value: {data.value}</p>
)}
</div>
);
}
return null;
};
const CustomNode = ({ x, y, width, height, payload, containerWidth }: any) => {
const isOut = x + width + 6 > containerWidth;
const nodeName = payload.name;
const color = COLORS[nodeName] || CHART_COLORS.defaultColor;
return (
<g>
<Rectangle
x={x}
y={y}
width={width}
height={height}
fill={color}
fillOpacity="1"
/>
<text
textAnchor={isOut ? "end" : "start"}
x={isOut ? x - 6 : x + width + 6}
y={y + height / 2}
fontSize="14"
className="fill-white stroke-white"
>
{nodeName}
</text>
<text
textAnchor={isOut ? "end" : "start"}
x={isOut ? x - 6 : x + width + 6}
y={y + height / 2 + 13}
fontSize="12"
className="fill-slate-400 stroke-slate-400"
strokeOpacity="0.5"
>
{payload.value}
</text>
</g>
);
};
const CustomLink = (props: any) => {
const {
sourceX,
targetX,
sourceY,
targetY,
sourceControlX,
targetControlX,
linkWidth,
} = props;
const sourceName = props.payload.source?.name || "";
const color = COLORS[sourceName] || CHART_COLORS.defaultColor;
return (
<g>
<path
d={`
M${sourceX},${sourceY + linkWidth / 2}
C${sourceControlX},${sourceY + linkWidth / 2}
${targetControlX},${targetY + linkWidth / 2}
${targetX},${targetY + linkWidth / 2}
L${targetX},${targetY - linkWidth / 2}
C${targetControlX},${targetY - linkWidth / 2}
${sourceControlX},${sourceY - linkWidth / 2}
${sourceX},${sourceY - linkWidth / 2}
Z
`}
fill={color}
fillOpacity="0.4"
stroke="none"
/>
</g>
);
};
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
return (
<ResponsiveContainer width="100%" height={height}>
<Sankey
data={data}
node={<CustomNode />}
link={<CustomLink />}
nodePadding={50}
margin={{ top: 20, right: 160, bottom: 20, left: 160 }}
>
<Tooltip content={<CustomTooltip />} />
</Sankey>
</ResponsiveContainer>
);
}

View File

@@ -5,7 +5,7 @@ import { Cell, Label, Pie, PieChart, Tooltip } from "recharts";
import { ChartConfig, ChartContainer } from "@/components/ui/chart/Chart";
import { ChartLegend } from "./shared/ChartLegend";
import { ChartLegend } from "./shared/chart-legend";
import { DonutDataPoint } from "./types";
interface DonutChartProps {
@@ -24,18 +24,30 @@ const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<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 text-white">
<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 text-slate-400">
<p
className="mt-2 text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
<span className="font-bold">
{data.change > 0 ? "+" : ""}
{data.change}%
@@ -145,14 +157,19 @@ export function DonutChart({
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-white text-3xl font-bold"
className="text-3xl font-bold"
style={{
fill: "var(--chart-text-primary)",
}}
>
{formattedValue}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-slate-400"
style={{
fill: "var(--chart-text-secondary)",
}}
>
{centerLabel.label}
</tspan>

View File

@@ -26,7 +26,12 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
<div className="w-full">
{title && (
<div className="mb-4">
<h3 className="text-lg font-semibold text-white">{title}</h3>
<h3
className="text-lg font-semibold"
style={{ color: "var(--chart-text-primary)" }}
>
{title}
</h3>
</div>
)}
@@ -48,8 +53,9 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
>
<div className="w-24 text-right">
<span
className="text-sm text-white"
className="text-sm"
style={{
color: "var(--chart-text-primary)",
opacity: isFaded ? 0.5 : 1,
transition: "opacity 0.2s",
}}
@@ -70,26 +76,44 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
/>
{isHovered && (
<div className="absolute top-10 left-0 z-10 min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<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 text-white">
<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} className="text-slate-400" />
<span className="text-sm text-slate-400">
<Bell
size={14}
style={{ color: "var(--chart-fail)" }}
/>
<span
className="text-sm"
style={{ color: "var(--chart-text-secondary)" }}
>
{item.newFindings} New Findings
</span>
</div>
)}
{item.change !== undefined && (
<p className="mt-1 text-sm text-slate-400">
<p
className="mt-1 text-sm"
style={{ color: "var(--chart-text-secondary)" }}
>
<span className="font-bold">
{item.change > 0 ? "+" : ""}
{item.change}%
@@ -102,15 +126,16 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
</div>
<div
className="flex w-40 items-center gap-2 text-sm text-white"
className="flex w-40 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 className="text-slate-400"></span>
<span>{item.value.toLocaleString()}</span>
<span style={{ color: "var(--chart-text-secondary)" }}></span>
<span className="font-bold">{item.value.toLocaleString()}</span>
</div>
</div>
);

View File

@@ -1,9 +1,9 @@
export { BarChart } from "./BarChart";
export { DonutChart } from "./DonutChart";
export { HorizontalBarChart } from "./HorizontalBarChart";
export { LineChart } from "./LineChart";
export { RadarChart } from "./RadarChart";
export { RadialChart } from "./RadialChart";
export { SankeyChart } from "./SankeyChart";
export { ScatterPlot } from "./ScatterPlot";
export { ChartLegend, type ChartLegendItem } from "./shared/ChartLegend";
export { DonutChart } from "./donut-chart";
export { HorizontalBarChart } from "./horizontal-bar-chart";
export { LineChart } from "./line-chart";
export { MapChart, type MapChartData, type MapChartProps } from "./map-chart";
export { RadarChart } from "./radar-chart";
export { RadialChart } from "./radial-chart";
export { SankeyChart } from "./sankey-chart";
export { ScatterPlot } from "./scatter-plot";
export { ChartLegend, type ChartLegendItem } from "./shared/chart-legend";

View File

@@ -14,8 +14,8 @@ import {
YAxis,
} from "recharts";
import { AlertPill } from "./shared/AlertPill";
import { ChartLegend } from "./shared/ChartLegend";
import { AlertPill } from "./shared/alert-pill";
import { ChartLegend } from "./shared/chart-legend";
import { CHART_COLORS } from "./shared/constants";
import { LineConfig, LineDataPoint } from "./types";
@@ -48,8 +48,19 @@ const CustomLineTooltip = ({
const totalValue = typedPayload.reduce((sum, item) => sum + item.value, 0);
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="mb-3 text-xs text-slate-400">{label}</p>
<div
className="rounded-lg border p-3 shadow-lg"
style={{
backgroundColor: "var(--chart-background)",
borderColor: "var(--chart-border-emphasis)",
}}
>
<p
className="mb-3 text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
{label}
</p>
<div className="mb-3">
<AlertPill value={totalValue} textSize="sm" />
@@ -67,18 +78,29 @@ const CustomLineTooltip = ({
className="h-2 w-2 rounded-full"
style={{ backgroundColor: item.stroke }}
/>
<span className="text-sm text-white">{item.value}</span>
<span
className="text-sm"
style={{ color: "var(--chart-text-primary)" }}
>
{item.value}
</span>
</div>
{newFindings !== undefined && (
<div className="flex items-center gap-2">
<Bell size={14} className="text-slate-400" />
<span className="text-xs text-slate-400">
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
<span
className="text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
{newFindings} New Findings
</span>
</div>
)}
{change !== undefined && typeof change === "number" && (
<p className="text-xs text-slate-400">
<p
className="text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
<span className="font-bold">
{change > 0 ? "+" : ""}
{change}%

View File

@@ -0,0 +1,479 @@
"use client";
import * as d3 from "d3";
import type {
Feature,
FeatureCollection,
GeoJsonProperties,
Geometry,
} from "geojson";
import { AlertTriangle, Info, MapPin } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { feature } from "topojson-client";
import type {
GeometryCollection,
Objects,
Topology,
} from "topojson-specification";
import { HorizontalBarChart } from "./horizontal-bar-chart";
import { BarDataPoint } from "./types";
// Constants
const MAP_CONFIG = {
defaultWidth: 688,
defaultHeight: 400,
pointRadius: 6,
selectedPointRadius: 8,
transitionDuration: 300,
} as const;
const MAP_COLORS = {
landFill: "var(--chart-border-emphasis)",
landStroke: "var(--chart-border)",
pointDefault: "#DB2B49",
pointSelected: "#86DA26",
pointHover: "#DB2B49",
} as const;
const RISK_LEVELS = {
LOW_HIGH: "low-high",
HIGH: "high",
CRITICAL: "critical",
} as const;
type RiskLevel = (typeof RISK_LEVELS)[keyof typeof RISK_LEVELS];
interface LocationPoint {
id: string;
name: string;
region: string;
coordinates: [number, number];
totalFindings: number;
riskLevel: RiskLevel;
severityData: BarDataPoint[];
change?: number;
}
export interface MapChartData {
locations: LocationPoint[];
regions: string[];
}
export interface MapChartProps {
data: MapChartData;
height?: number;
onLocationSelect?: (location: LocationPoint | null) => void;
}
// Utility functions
function createProjection(width: number, height: number) {
return d3
.geoNaturalEarth1()
.fitExtent(
[
[1, 1],
[width - 1, height - 1],
],
{ type: "Sphere" },
)
.precision(0.2);
}
async function fetchWorldData(): Promise<FeatureCollection | null> {
try {
const worldAtlasModule = await import("world-atlas/countries-110m.json");
const worldData = worldAtlasModule.default || worldAtlasModule;
const topology = worldData as unknown as Topology<Objects>;
return feature(
topology,
topology.objects.countries as GeometryCollection,
) as FeatureCollection;
} catch (error) {
console.error("Error loading world map data:", error);
return null;
}
}
// Helper: Create SVG element
function createSVGElement<T extends SVGElement>(
type: string,
attributes: Record<string, string>,
): T {
const element = document.createElementNS(
"http://www.w3.org/2000/svg",
type,
) as T;
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
return element;
}
// Components
function MapTooltip({
location,
position,
}: {
location: LocationPoint;
position: { x: number; y: number };
}) {
const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textPrimary: "var(--chart-text-primary)",
textSecondary: "var(--chart-text-secondary)",
};
return (
<div
className="pointer-events-none absolute z-50 min-w-[200px] rounded-lg border p-3 shadow-lg"
style={{
left: `${position.x + 15}px`,
top: `${position.y + 15}px`,
transform: "translate(0, -50%)",
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="flex items-center gap-2">
<MapPin size={14} style={{ color: CHART_COLORS.textSecondary }} />
<span
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{location.name}
</span>
</div>
<div className="mt-1 flex items-center gap-2">
<AlertTriangle size={14} className="text-[#DB2B49]" />
<span className="text-sm" style={{ color: CHART_COLORS.textPrimary }}>
{location.totalFindings.toLocaleString()} Fail Findings
</span>
</div>
{location.change !== undefined && (
<p
className="mt-1 text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
<span className="font-bold">
{location.change > 0 ? "+" : ""}
{location.change}%
</span>{" "}
since last scan
</p>
)}
</div>
);
}
function EmptyState() {
const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textSecondary: "var(--chart-text-secondary)",
};
return (
<div
className="flex h-full min-h-[400px] items-center justify-center rounded-lg border p-6"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="text-center">
<Info
size={48}
className="mx-auto mb-2"
style={{ color: CHART_COLORS.textSecondary }}
/>
<p className="text-sm" style={{ color: CHART_COLORS.textSecondary }}>
Select a location on the map to view details
</p>
</div>
</div>
);
}
function LoadingState({ height }: { height: number }) {
const CHART_COLORS = {
textSecondary: "var(--chart-text-secondary)",
};
return (
<div className="flex items-center justify-center" style={{ height }}>
<div className="text-center">
<div className="mb-2" style={{ color: CHART_COLORS.textSecondary }}>
Loading map...
</div>
</div>
</div>
);
}
export function MapChart({
data,
height = MAP_CONFIG.defaultHeight,
}: MapChartProps) {
const svgRef = useRef<SVGSVGElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [selectedLocation, setSelectedLocation] =
useState<LocationPoint | null>(null);
const [hoveredLocation, setHoveredLocation] = useState<LocationPoint | null>(
null,
);
const [tooltipPosition, setTooltipPosition] = useState<{
x: number;
y: number;
} | null>(null);
const [worldData, setWorldData] = useState<FeatureCollection | null>(null);
const [isLoadingMap, setIsLoadingMap] = useState(true);
const [dimensions, setDimensions] = useState<{
width: number;
height: number;
}>({
width: MAP_CONFIG.defaultWidth,
height,
});
// Fetch world data once on mount
useEffect(() => {
let isMounted = true;
fetchWorldData()
.then((data) => {
if (isMounted && data) setWorldData(data);
})
.catch(console.error)
.finally(() => {
if (isMounted) setIsLoadingMap(false);
});
return () => {
isMounted = false;
};
}, []);
// Update dimensions on resize
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
setDimensions({ width: containerRef.current.clientWidth, height });
}
};
updateDimensions();
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, [height]);
// Render the map
useEffect(() => {
if (!svgRef.current || !worldData || isLoadingMap) return;
const svg = svgRef.current;
const { width, height } = dimensions;
svg.innerHTML = "";
const projection = createProjection(width, height);
const path = d3.geoPath().projection(projection);
// Render countries
const mapGroup = createSVGElement<SVGGElement>("g", {
class: "map-countries",
});
worldData.features?.forEach(
(feature: Feature<Geometry, GeoJsonProperties>) => {
const pathData = path(feature);
if (pathData) {
const pathElement = createSVGElement<SVGPathElement>("path", {
d: pathData,
fill: MAP_COLORS.landFill,
stroke: MAP_COLORS.landStroke,
"stroke-width": "0.5",
});
mapGroup.appendChild(pathElement);
}
},
);
svg.appendChild(mapGroup);
// Helper to update tooltip position
const updateTooltip = (e: MouseEvent) => {
const rect = svg.getBoundingClientRect();
setTooltipPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
};
// Helper to create circle
const createCircle = (location: LocationPoint) => {
const projected = projection(location.coordinates);
if (!projected) return null;
const [x, y] = projected;
if (x < 0 || x > width || y < 0 || y > height) return null;
const isSelected = selectedLocation?.id === location.id;
const isHovered = hoveredLocation?.id === location.id;
const classes = ["cursor-pointer"];
if (isSelected) classes.push("drop-shadow-[0_0_8px_#86da26]");
if (isHovered && !isSelected) classes.push("opacity-70");
const circle = createSVGElement<SVGCircleElement>("circle", {
cx: x.toString(),
cy: y.toString(),
r: (isSelected
? MAP_CONFIG.selectedPointRadius
: MAP_CONFIG.pointRadius
).toString(),
fill: isSelected ? MAP_COLORS.pointSelected : MAP_COLORS.pointDefault,
class: classes.join(" "),
});
circle.addEventListener("click", () =>
setSelectedLocation(isSelected ? null : location),
);
circle.addEventListener("mouseenter", (e) => {
setHoveredLocation(location);
updateTooltip(e);
});
circle.addEventListener("mousemove", updateTooltip);
circle.addEventListener("mouseleave", () => {
setHoveredLocation(null);
setTooltipPosition(null);
});
return circle;
};
// Render points
const pointsGroup = createSVGElement<SVGGElement>("g", {
class: "threat-points",
});
// Unselected points first
data.locations.forEach((location) => {
if (selectedLocation?.id !== location.id) {
const circle = createCircle(location);
if (circle) pointsGroup.appendChild(circle);
}
});
// Selected point last (on top)
if (selectedLocation) {
const selectedData = data.locations.find(
(loc) => loc.id === selectedLocation.id,
);
if (selectedData) {
const circle = createCircle(selectedData);
if (circle) pointsGroup.appendChild(circle);
}
}
svg.appendChild(pointsGroup);
}, [
data.locations,
dimensions,
selectedLocation,
hoveredLocation,
worldData,
isLoadingMap,
]);
const CHART_COLORS = {
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textPrimary: "var(--chart-text-primary)",
textSecondary: "var(--chart-text-secondary)",
};
return (
<div className="flex w-full flex-col gap-6 lg:flex-row lg:items-start">
{/* Map Section */}
<div className="flex-1">
<h3
className="mb-4 text-lg font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
Threat Map
</h3>
<div
ref={containerRef}
className="rounded-lg border p-4"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
{isLoadingMap ? (
<LoadingState height={dimensions.height} />
) : (
<>
<div className="relative">
<svg
ref={svgRef}
width={dimensions.width}
height={dimensions.height}
className="w-full"
style={{ maxWidth: "100%" }}
/>
{hoveredLocation && tooltipPosition && (
<MapTooltip
location={hoveredLocation}
position={tooltipPosition}
/>
)}
</div>
<div className="mt-4 flex items-center gap-2">
<div className="h-3 w-3 rounded-full bg-[#DB2B49]" />
<span
className="text-sm"
style={{ color: CHART_COLORS.textSecondary }}
>
{data.locations.length} Locations
</span>
</div>
</>
)}
</div>
</div>
{/* Details Section */}
<div className="w-full lg:w-[400px]">
<div className="mb-4 h-10" />
{selectedLocation ? (
<div
className="rounded-lg border p-6"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="mb-6">
<div className="mb-1 flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-[#86DA26]" />
<h4
className="text-base font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{selectedLocation.name}
</h4>
</div>
<p
className="text-sm"
style={{ color: CHART_COLORS.textSecondary }}
>
{selectedLocation.totalFindings.toLocaleString()} Total Findings
</p>
</div>
<HorizontalBarChart data={selectedLocation.severityData} />
</div>
) : (
<EmptyState />
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,50 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select/Select";
interface MapRegionFilterProps {
regions: string[];
selectedRegion: string;
onRegionChange: (region: string) => void;
chartColors: {
tooltipBorder: string;
tooltipBackground: string;
textPrimary: string;
};
}
export function MapRegionFilter({
regions,
selectedRegion,
onRegionChange,
chartColors,
}: MapRegionFilterProps) {
return (
<Select value={selectedRegion} onValueChange={onRegionChange}>
<SelectTrigger
className="min-w-[200px] rounded-lg"
style={{
borderColor: chartColors.tooltipBorder,
backgroundColor: chartColors.tooltipBackground,
color: chartColors.textPrimary,
}}
>
<SelectValue placeholder="All Regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value="All Regions">All Regions</SelectItem>
{regions.map((region) => (
<SelectItem key={region} value={region}>
{region}
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@@ -13,7 +13,7 @@ import {
ChartTooltip,
} from "@/components/ui/chart/Chart";
import { AlertPill } from "./shared/AlertPill";
import { AlertPill } from "./shared/alert-pill";
import { CHART_COLORS } from "./shared/constants";
import { RadarDataPoint } from "./types";
@@ -28,7 +28,7 @@ interface RadarChartProps {
const chartConfig = {
value: {
label: "Findings",
color: "var(--color-magenta)",
color: "var(--chart-radar-primary)",
},
} satisfies ChartConfig;
@@ -36,15 +36,27 @@ const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0];
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="text-sm font-semibold text-white">
<div
className="rounded-lg border p-3 shadow-lg"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{data.payload.category}
</p>
<div className="mt-1">
<AlertPill value={data.value} />
</div>
{data.payload.change !== undefined && (
<p className="mt-1 text-xs text-slate-400">
<p
className="mt-1 text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
<span className="font-bold">
{data.payload.change > 0 ? "+" : ""}
{data.payload.change}%
@@ -84,8 +96,11 @@ const CustomDot = (props: any) => {
cx={cx}
cy={cy}
r={isSelected ? 9 : 6}
fill={isSelected ? "var(--color-success)" : "var(--color-purple-dark)"}
fill={
isSelected ? "var(--chart-success-color)" : "var(--chart-radar-primary)"
}
fillOpacity={1}
className={isSelected ? "drop-shadow-[0_0_8px_#86da26]" : ""}
style={{
cursor: onSelectPoint ? "pointer" : "default",
pointerEvents: "all",
@@ -117,7 +132,7 @@ export function RadarChart({
<PolarGrid strokeOpacity={0.3} />
<Radar
dataKey={dataKey}
fill="var(--color-magenta)"
fill="var(--chart-radar-primary)"
fillOpacity={0.2}
activeDot={false}
dot={
@@ -135,7 +150,7 @@ export function RadarChart({
}
: {
r: 6,
fill: "var(--color-purple-dark)",
fill: "var(--chart-radar-primary)",
fillOpacity: 1,
}
}

View File

@@ -23,7 +23,7 @@ interface RadialChartProps {
export function RadialChart({
percentage,
label = "Score",
color = "var(--color-success)",
color = "var(--chart-success-color)",
backgroundColor = CHART_COLORS.tooltipBackground,
height = 250,
innerRadius = 60,
@@ -68,7 +68,10 @@ export function RadialChart({
y="50%"
textAnchor="middle"
dominantBaseline="middle"
className="fill-white text-4xl font-bold"
className="text-4xl font-bold"
style={{
fill: "var(--chart-text-primary)",
}}
>
{percentage}%
</text>

View File

@@ -0,0 +1,403 @@
"use client";
import { useState } from "react";
import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts";
import { ChartTooltip } from "./shared/chart-tooltip";
import { CHART_COLORS } from "./shared/constants";
interface SankeyNode {
name: string;
newFindings?: number;
change?: number;
}
interface SankeyLink {
source: number;
target: number;
value: number;
}
interface SankeyChartProps {
data: {
nodes: SankeyNode[];
links: SankeyLink[];
};
height?: number;
}
interface LinkTooltipState {
show: boolean;
x: number;
y: number;
sourceName: string;
targetName: string;
value: number;
color: string;
}
interface NodeTooltipState {
show: boolean;
x: number;
y: number;
name: string;
value: number;
color: string;
newFindings?: number;
change?: number;
}
// Note: Using hex colors directly because Recharts SVG fill doesn't resolve CSS variables
const COLORS: Record<string, string> = {
Success: "#86da26",
Fail: "#db2b49",
AWS: "#ff9900",
Azure: "#00bcd4",
Google: "#EA4335",
Critical: "#971348",
High: "#ff3077",
Medium: "#ff7d19",
Low: "#fdd34f",
Info: "#2e51b2",
Informational: "#2e51b2",
};
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={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{data.name}
</p>
{data.value && (
<p className="text-xs" style={{ color: CHART_COLORS.textSecondary }}>
Value: {data.value}
</p>
)}
</div>
);
}
return null;
};
const CustomNode = (props: any) => {
const { x, y, width, height, payload, containerWidth } = props;
const isOut = x + width + 6 > containerWidth;
const nodeName = payload.name;
const color = COLORS[nodeName] || CHART_COLORS.defaultColor;
const isHidden = nodeName === "";
const hasTooltip = !isHidden && payload.newFindings;
const handleMouseEnter = (e: React.MouseEvent) => {
if (!hasTooltip) return;
const rect = e.currentTarget.closest("svg") as SVGSVGElement;
if (rect) {
const bbox = rect.getBoundingClientRect();
props.onNodeHover?.({
x: e.clientX - bbox.left,
y: e.clientY - bbox.top,
name: nodeName,
value: payload.value,
color,
newFindings: payload.newFindings,
change: payload.change,
});
}
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!hasTooltip) return;
const rect = e.currentTarget.closest("svg") as SVGSVGElement;
if (rect) {
const bbox = rect.getBoundingClientRect();
props.onNodeMove?.({
x: e.clientX - bbox.left,
y: e.clientY - bbox.top,
});
}
};
const handleMouseLeave = () => {
if (!hasTooltip) return;
props.onNodeLeave?.();
};
return (
<g
style={{ cursor: hasTooltip ? "pointer" : "default" }}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Rectangle
x={x}
y={y}
width={width}
height={height}
fill={color}
fillOpacity={isHidden ? "0" : "1"}
/>
{!isHidden && (
<>
<text
textAnchor={isOut ? "end" : "start"}
x={isOut ? x - 6 : x + width + 6}
y={y + height / 2}
fontSize="14"
fill={CHART_COLORS.textPrimary}
>
{nodeName}
</text>
<text
textAnchor={isOut ? "end" : "start"}
x={isOut ? x - 6 : x + width + 6}
y={y + height / 2 + 13}
fontSize="12"
fill={CHART_COLORS.textSecondary}
>
{payload.value}
</text>
</>
)}
</g>
);
};
const CustomLink = (props: any) => {
const {
sourceX,
targetX,
sourceY,
targetY,
sourceControlX,
targetControlX,
linkWidth,
index,
} = props;
const sourceName = props.payload.source?.name || "";
const targetName = props.payload.target?.name || "";
const value = props.payload.value || 0;
const color = COLORS[sourceName] || CHART_COLORS.defaultColor;
const isHidden = targetName === "";
const isHovered = props.hoveredLink !== null && props.hoveredLink === index;
const hasHoveredLink = props.hoveredLink !== null;
const pathD = `
M${sourceX},${sourceY + linkWidth / 2}
C${sourceControlX},${sourceY + linkWidth / 2}
${targetControlX},${targetY + linkWidth / 2}
${targetX},${targetY + linkWidth / 2}
L${targetX},${targetY - linkWidth / 2}
C${targetControlX},${targetY - linkWidth / 2}
${sourceControlX},${sourceY - linkWidth / 2}
${sourceX},${sourceY - linkWidth / 2}
Z
`;
const getOpacity = () => {
if (isHidden) return "0";
if (!hasHoveredLink) return "0.4";
return isHovered ? "0.8" : "0.1";
};
const handleMouseEnter = (e: React.MouseEvent) => {
const rect = e.currentTarget.parentElement?.parentElement
?.parentElement as unknown as SVGSVGElement;
if (rect) {
const bbox = rect.getBoundingClientRect();
props.onLinkHover?.(index, {
x: e.clientX - bbox.left,
y: e.clientY - bbox.top,
sourceName,
targetName,
value,
color,
});
}
};
const handleMouseMove = (e: React.MouseEvent) => {
const rect = e.currentTarget.parentElement?.parentElement
?.parentElement as unknown as SVGSVGElement;
if (rect && isHovered) {
const bbox = rect.getBoundingClientRect();
props.onLinkMove?.({
x: e.clientX - bbox.left,
y: e.clientY - bbox.top,
});
}
};
const handleMouseLeave = () => {
props.onLinkLeave?.();
};
return (
<g>
<path
d={pathD}
fill={color}
fillOpacity={getOpacity()}
stroke="none"
style={{ cursor: "pointer", transition: "fill-opacity 0.2s" }}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
/>
</g>
);
};
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
const [linkTooltip, setLinkTooltip] = useState<LinkTooltipState>({
show: false,
x: 0,
y: 0,
sourceName: "",
targetName: "",
value: 0,
color: "",
});
const [nodeTooltip, setNodeTooltip] = useState<NodeTooltipState>({
show: false,
x: 0,
y: 0,
name: "",
value: 0,
color: "",
});
const handleLinkHover = (
index: number,
data: Omit<LinkTooltipState, "show">,
) => {
setHoveredLink(index);
setLinkTooltip({ show: true, ...data });
};
const handleLinkMove = (position: { x: number; y: number }) => {
setLinkTooltip((prev) => ({
...prev,
x: position.x,
y: position.y,
}));
};
const handleLinkLeave = () => {
setHoveredLink(null);
setLinkTooltip((prev) => ({ ...prev, show: false }));
};
const handleNodeHover = (data: Omit<NodeTooltipState, "show">) => {
setNodeTooltip({ show: true, ...data });
};
const handleNodeMove = (position: { x: number; y: number }) => {
setNodeTooltip((prev) => ({
...prev,
x: position.x,
y: position.y,
}));
};
const handleNodeLeave = () => {
setNodeTooltip((prev) => ({ ...prev, show: false }));
};
return (
<div className="relative">
<ResponsiveContainer width="100%" height={height}>
<Sankey
data={data}
node={
<CustomNode
onNodeHover={handleNodeHover}
onNodeMove={handleNodeMove}
onNodeLeave={handleNodeLeave}
/>
}
link={
<CustomLink
hoveredLink={hoveredLink}
onLinkHover={handleLinkHover}
onLinkMove={handleLinkMove}
onLinkLeave={handleLinkLeave}
/>
}
nodePadding={50}
margin={{ top: 20, right: 160, bottom: 20, left: 160 }}
sort={false}
>
<Tooltip content={<CustomTooltip />} />
</Sankey>
</ResponsiveContainer>
{linkTooltip.show && (
<div
className="pointer-events-none absolute z-50"
style={{
left: `${Math.max(125, Math.min(linkTooltip.x, window.innerWidth - 125))}px`,
top: `${Math.max(linkTooltip.y - 80, 10)}px`,
transform: "translate(-50%, -100%)",
}}
>
<ChartTooltip
active={true}
payload={[
{
payload: {
name: linkTooltip.targetName,
value: linkTooltip.value,
color: linkTooltip.color,
},
color: linkTooltip.color,
},
]}
label={`${linkTooltip.sourceName}${linkTooltip.targetName}`}
/>
</div>
)}
{nodeTooltip.show && (
<div
className="pointer-events-none absolute z-50"
style={{
left: `${Math.max(125, Math.min(nodeTooltip.x, window.innerWidth - 125))}px`,
top: `${Math.max(nodeTooltip.y - 80, 10)}px`,
transform: "translate(-50%, -100%)",
}}
>
<ChartTooltip
active={true}
payload={[
{
payload: {
name: nodeTooltip.name,
value: nodeTooltip.value,
color: nodeTooltip.color,
newFindings: nodeTooltip.newFindings,
change: nodeTooltip.change,
},
color: nodeTooltip.color,
},
]}
/>
</div>
)}
</div>
);
}

View File

@@ -11,18 +11,11 @@ import {
YAxis,
} from "recharts";
import { AlertPill } from "./shared/AlertPill";
import { ChartLegend } from "./shared/ChartLegend";
import { AlertPill } from "./shared/alert-pill";
import { ChartLegend } from "./shared/chart-legend";
import { CHART_COLORS } from "./shared/constants";
import { getSeverityColorByRiskScore } from "./shared/utils";
interface ScatterDataPoint {
x: number;
y: number;
provider: string;
name: string;
size?: number;
}
import type { ScatterDataPoint } from "./types";
interface ScatterPlotProps {
data: ScatterDataPoint[];
@@ -34,9 +27,9 @@ interface ScatterPlotProps {
}
const PROVIDER_COLORS = {
AWS: "var(--color-orange)",
Azure: "var(--color-cyan)",
Google: "var(--color-red)",
AWS: "var(--chart-provider-aws)",
Azure: "var(--chart-provider-azure)",
Google: "var(--chart-provider-google)",
};
const CustomTooltip = ({ active, payload }: any) => {
@@ -45,9 +38,23 @@ const CustomTooltip = ({ active, payload }: any) => {
const severityColor = getSeverityColorByRiskScore(data.x);
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="text-sm font-semibold text-white">{data.name}</p>
<p className="mt-1 text-xs text-slate-400">
<div
className="rounded-lg border p-3 shadow-lg"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{data.name}
</p>
<p
className="mt-1 text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
<span style={{ color: severityColor }}>{data.x}</span> Risk Score
</p>
<div className="mt-2">
@@ -69,7 +76,7 @@ const CustomScatterDot = ({
const isSelected = selectedPoint?.name === payload.name;
const size = isSelected ? 18 : 8;
const fill = isSelected
? "var(--color-success)"
? "#86DA26"
: PROVIDER_COLORS[payload.provider as keyof typeof PROVIDER_COLORS] ||
CHART_COLORS.defaultColor;
@@ -79,8 +86,9 @@ const CustomScatterDot = ({
cy={cy}
r={size / 2}
fill={fill}
stroke={isSelected ? "var(--color-success)" : "transparent"}
stroke={isSelected ? "#86DA26" : "transparent"}
strokeWidth={2}
className={isSelected ? "drop-shadow-[0_0_8px_#86da26]" : ""}
style={{ cursor: "pointer" }}
onClick={() => onSelectPoint?.(payload)}
/>

View File

@@ -17,13 +17,17 @@ export function AlertPill({
}: AlertPillProps) {
return (
<div className="flex items-center gap-2">
<div className="bg-alert-pill-bg flex items-center gap-1 rounded-full px-2 py-1">
<AlertTriangle size={iconSize} className="text-alert-pill-text" />
<div
className="flex items-center gap-1 rounded-full px-2 py-1"
style={{ backgroundColor: "var(--chart-alert-bg)" }}
>
<AlertTriangle
size={iconSize}
style={{ color: "var(--chart-alert-text)" }}
/>
<span
className={cn(
`text-${textSize}`,
"text-alert-pill-text font-semibold",
)}
className={cn(`text-${textSize}`, "font-semibold")}
style={{ color: "var(--chart-alert-text)" }}
>
{value}
</span>

View File

@@ -9,14 +9,22 @@ interface ChartLegendProps {
export function ChartLegend({ items }: ChartLegendProps) {
return (
<div className="bg-card-border mt-4 inline-flex gap-[46px] rounded-full border-2 px-[19px] py-[9px]">
<div
className="mt-4 inline-flex gap-[46px] rounded-full border-2 px-[19px] py-[9px]"
style={{ borderColor: "var(--chart-border)" }}
>
{items.map((item, index) => (
<div key={`legend-${index}`} className="flex items-center gap-1">
<div
className="h-3 w-3 rounded"
style={{ backgroundColor: item.color }}
/>
<span className="text-xs text-gray-300">{item.label}</span>
<span
className="text-xs"
style={{ color: "var(--chart-text-secondary)" }}
>
{item.label}
</span>
</div>
))}
</div>

View File

@@ -3,6 +3,7 @@ import { Bell, VolumeX } from "lucide-react";
import { cn } from "@/lib/utils";
import { TooltipData } from "../types";
import { CHART_COLORS } from "./constants";
interface ChartTooltipProps {
active?: boolean;
@@ -27,7 +28,13 @@ export function ChartTooltip({
const color = payload[0].color || data.color;
return (
<div className="min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<div
className="min-w-[200px] rounded-lg border p-3 shadow-lg"
style={{
borderColor: CHART_COLORS.tooltipBorder,
backgroundColor: CHART_COLORS.tooltipBackground,
}}
>
<div className="flex items-center gap-2">
{showColorIndicator && color && (
<div
@@ -38,10 +45,15 @@ export function ChartTooltip({
style={{ backgroundColor: color }}
/>
)}
<p className="text-sm font-semibold text-white">{label || data.name}</p>
<p
className="text-sm font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{label || data.name}
</p>
</div>
<p className="mt-1 text-xs text-white">
<p className="mt-1 text-xs" style={{ color: CHART_COLORS.textPrimary }}>
{typeof data.value === "number"
? data.value.toLocaleString()
: data.value}
@@ -50,8 +62,11 @@ export function ChartTooltip({
{data.newFindings !== undefined && data.newFindings > 0 && (
<div className="mt-1 flex items-center gap-2">
<Bell size={14} className="text-slate-400" />
<span className="text-xs text-slate-400">
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
<span
className="text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
{data.newFindings} New Findings
</span>
</div>
@@ -59,20 +74,33 @@ export function ChartTooltip({
{data.new !== undefined && data.new > 0 && (
<div className="mt-1 flex items-center gap-2">
<Bell size={14} className="text-slate-400" />
<span className="text-xs text-slate-400">{data.new} New</span>
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
<span
className="text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
{data.new} New
</span>
</div>
)}
{data.muted !== undefined && data.muted > 0 && (
<div className="mt-1 flex items-center gap-2">
<VolumeX size={14} className="text-slate-400" />
<span className="text-xs text-slate-400">{data.muted} Muted</span>
<VolumeX size={14} style={{ color: CHART_COLORS.textSecondary }} />
<span
className="text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
{data.muted} Muted
</span>
</div>
)}
{data.change !== undefined && (
<p className="mt-1 text-xs text-slate-400">
<p
className="mt-1 text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
<span className="font-bold">
{data.change > 0 ? "+" : ""}
{data.change}%
@@ -97,8 +125,19 @@ export function MultiSeriesChartTooltip({
}
return (
<div className="min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
<p className="mb-2 text-sm font-semibold text-white">{label}</p>
<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 }}
>
{label}
</p>
{payload.map((entry: any, index: number) => (
<div key={index} className="flex items-center gap-2">
@@ -106,12 +145,20 @@ export function MultiSeriesChartTooltip({
className="h-2 w-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span className="text-xs text-white">{entry.name}:</span>
<span className="text-xs font-semibold text-white">
<span className="text-xs" style={{ color: CHART_COLORS.textPrimary }}>
{entry.name}:
</span>
<span
className="text-xs font-semibold"
style={{ color: CHART_COLORS.textPrimary }}
>
{entry.value}
</span>
{entry.payload[`${entry.dataKey}_change`] && (
<span className="text-xs text-slate-400">
<span
className="text-xs"
style={{ color: CHART_COLORS.textSecondary }}
>
({entry.payload[`${entry.dataKey}_change`] > 0 ? "+" : ""}
{entry.payload[`${entry.dataKey}_change`]}%)
</span>

View File

@@ -1,21 +1,33 @@
export const SEVERITY_COLORS = {
Informational: "var(--color-info)",
Low: "var(--color-warning)",
Medium: "var(--color-warning-emphasis)",
High: "var(--color-danger)",
Critical: "var(--color-danger-emphasis)",
Informational: "var(--chart-info)",
Info: "var(--chart-info)",
Low: "var(--chart-warning)",
Medium: "var(--chart-warning-emphasis)",
High: "var(--chart-danger)",
Critical: "var(--chart-danger-emphasis)",
} as const;
export const PROVIDER_COLORS = {
AWS: "var(--chart-provider-aws)",
Azure: "var(--chart-provider-azure)",
Google: "var(--chart-provider-google)",
} as const;
export const STATUS_COLORS = {
Success: "var(--chart-success-color)",
Fail: "var(--chart-fail)",
} as const;
export const CHART_COLORS = {
tooltipBorder: "var(--color-slate-700)",
tooltipBackground: "var(--color-slate-800)",
textPrimary: "var(--color-white)",
textSecondary: "var(--color-slate-400)",
gridLine: "var(--color-slate-700)",
tooltipBorder: "var(--chart-border-emphasis)",
tooltipBackground: "var(--chart-background)",
textPrimary: "var(--chart-text-primary)",
textSecondary: "var(--chart-text-secondary)",
gridLine: "var(--chart-border-emphasis)",
backgroundTrack: "rgba(51, 65, 85, 0.5)", // slate-700 with 50% opacity
alertPillBg: "var(--color-alert-pill-bg)",
alertPillText: "var(--color-alert-pill-text)",
defaultColor: "var(--color-slate-500)", // Default fallback color for charts
alertPillBg: "var(--chart-alert-bg)",
alertPillText: "var(--chart-alert-text)",
defaultColor: "#64748b", // slate-500
} as const;
export const CHART_DIMENSIONS = {

View File

@@ -36,6 +36,14 @@ export interface RadarDataPoint {
change?: number;
}
export interface ScatterDataPoint {
x: number;
y: number;
provider: string;
name: string;
size?: number;
}
export interface LineConfig {
dataKey: string;
color: string;

View File

@@ -12,6 +12,6 @@ export * from "./feedback-banner/feedback-banner";
export * from "./headers/navigation-header";
export * from "./label/Label";
export * from "./main-layout/main-layout";
export * from "./select/Select";
export * from "./select";
export * from "./sidebar";
export * from "./toast";

View File

@@ -0,0 +1,12 @@
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "./Select";

View File

@@ -5,7 +5,7 @@
"from": "1.0.59",
"to": "1.0.59",
"strategy": "installed",
"generatedAt": "2025-10-01T11:13:12.025Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -13,7 +13,7 @@
"from": "2.0.59",
"to": "2.0.59",
"strategy": "installed",
"generatedAt": "2025-10-01T11:13:12.025Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -21,15 +21,15 @@
"from": "2.8.4",
"to": "2.8.4",
"strategy": "installed",
"generatedAt": "2025-09-29T14:26:25.838Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@hookform/resolvers",
"from": "3.10.0",
"from": "5.2.2",
"to": "5.2.2",
"strategy": "installed",
"generatedAt": "2025-10-01T15:09:44.056Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -37,7 +37,7 @@
"from": "0.3.77",
"to": "0.3.77",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -45,23 +45,23 @@
"from": "0.4.9",
"to": "0.4.9",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@langchain/langgraph-supervisor",
"from": "0.0.12",
"from": "0.0.20",
"to": "0.0.20",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "@langchain/openai",
"from": "0.6.9",
"from": "0.5.18",
"to": "0.5.18",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -69,7 +69,7 @@
"from": "15.3.5",
"to": "15.3.5",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -77,7 +77,7 @@
"from": "1.1.14",
"to": "1.1.14",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -85,7 +85,7 @@
"from": "1.1.14",
"to": "1.1.14",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -93,7 +93,7 @@
"from": "2.1.15",
"to": "2.1.15",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -101,7 +101,7 @@
"from": "1.3.2",
"to": "1.3.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -109,7 +109,7 @@
"from": "2.1.7",
"to": "2.1.7",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -117,7 +117,7 @@
"from": "2.2.5",
"to": "2.2.5",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -125,7 +125,7 @@
"from": "1.2.3",
"to": "1.2.3",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -133,7 +133,7 @@
"from": "1.2.14",
"to": "1.2.14",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -141,7 +141,7 @@
"from": "3.9.4",
"to": "3.9.4",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -149,7 +149,7 @@
"from": "3.8.12",
"to": "3.8.12",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -157,7 +157,7 @@
"from": "4.1.13",
"to": "4.1.13",
"strategy": "installed",
"generatedAt": "2025-09-24T15:04:48.761Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -165,7 +165,7 @@
"from": "0.5.16",
"to": "0.5.16",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -173,7 +173,7 @@
"from": "8.21.3",
"to": "8.21.3",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -181,15 +181,15 @@
"from": "4.0.9",
"to": "4.0.9",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "ai",
"from": "4.3.16",
"from": "5.0.59",
"to": "5.0.59",
"strategy": "installed",
"generatedAt": "2025-10-01T10:03:22.788Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -197,7 +197,7 @@
"from": "6.0.2",
"to": "6.0.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -205,7 +205,7 @@
"from": "0.7.1",
"to": "0.7.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -213,7 +213,15 @@
"from": "2.1.1",
"to": "2.1.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "d3",
"from": "7.9.0",
"to": "7.9.0",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -221,7 +229,7 @@
"from": "4.1.0",
"to": "4.1.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -229,7 +237,7 @@
"from": "11.18.2",
"to": "11.18.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -237,7 +245,7 @@
"from": "10.7.16",
"to": "10.7.16",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -245,7 +253,7 @@
"from": "5.10.0",
"to": "5.10.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -253,7 +261,7 @@
"from": "4.1.0",
"to": "4.1.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -261,7 +269,7 @@
"from": "4.0.0",
"to": "4.0.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -269,7 +277,7 @@
"from": "0.543.0",
"to": "0.543.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -277,15 +285,15 @@
"from": "15.0.12",
"to": "15.0.12",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "next",
"from": "14.2.32",
"from": "15.5.3",
"to": "15.5.3",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -293,7 +301,7 @@
"from": "5.0.0-beta.29",
"to": "5.0.0-beta.29",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -301,7 +309,7 @@
"from": "0.2.1",
"to": "0.2.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -309,23 +317,23 @@
"from": "1.4.2",
"to": "1.4.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "react",
"from": "18.3.1",
"from": "19.1.1",
"to": "19.1.1",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "react-dom",
"from": "18.3.1",
"from": "19.1.1",
"to": "19.1.1",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -333,7 +341,7 @@
"from": "7.62.0",
"to": "7.62.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -341,7 +349,7 @@
"from": "10.1.0",
"to": "10.1.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -349,7 +357,7 @@
"from": "2.15.4",
"to": "2.15.4",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -357,7 +365,7 @@
"from": "3.13.0",
"to": "3.13.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -365,15 +373,7 @@
"from": "0.0.1",
"to": "0.0.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
},
{
"section": "dependencies",
"name": "shadcn",
"from": "3.2.1",
"to": "3.2.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -381,7 +381,7 @@
"from": "0.33.5",
"to": "0.33.5",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -389,7 +389,7 @@
"from": "3.3.1",
"to": "3.3.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -397,7 +397,15 @@
"from": "1.0.7",
"to": "1.0.7",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "topojson-client",
"from": "3.1.0",
"to": "3.1.0",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -405,7 +413,7 @@
"from": "1.4.0",
"to": "1.4.0",
"strategy": "installed",
"generatedAt": "2025-10-15T07:57:13.225Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
@@ -413,23 +421,31 @@
"from": "11.1.0",
"to": "11.1.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.548Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "world-atlas",
"from": "2.0.2",
"to": "2.0.2",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "zod",
"from": "3.25.73",
"from": "4.1.11",
"to": "4.1.11",
"strategy": "installed",
"generatedAt": "2025-10-01T09:40:25.207Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "dependencies",
"name": "zustand",
"from": "4.5.7",
"from": "5.0.8",
"to": "5.0.8",
"strategy": "installed",
"generatedAt": "2025-10-01T09:40:25.207Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -437,15 +453,23 @@
"from": "5.2.1",
"to": "5.2.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "@playwright/test",
"from": "1.53.2",
"to": "1.53.2",
"from": "1.56.1",
"to": "1.56.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "@types/d3",
"from": "7.4.3",
"to": "7.4.3",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -453,23 +477,31 @@
"from": "20.5.7",
"to": "20.5.7",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "@types/react",
"from": "18.3.3",
"from": "19.1.13",
"to": "19.1.13",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "@types/react-dom",
"from": "18.3.0",
"from": "19.1.9",
"to": "19.1.9",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "@types/topojson-client",
"from": "3.1.5",
"to": "3.1.5",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -477,7 +509,7 @@
"from": "10.0.0",
"to": "10.0.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -485,7 +517,7 @@
"from": "7.18.0",
"to": "7.18.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -493,7 +525,7 @@
"from": "7.18.0",
"to": "7.18.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -501,7 +533,7 @@
"from": "10.4.19",
"to": "10.4.19",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -509,7 +541,7 @@
"from": "19.1.0-rc.3",
"to": "19.1.0-rc.3",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -517,15 +549,15 @@
"from": "8.57.1",
"to": "8.57.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "eslint-config-next",
"from": "14.2.32",
"from": "15.5.3",
"to": "15.5.3",
"strategy": "installed",
"generatedAt": "2025-09-23T10:22:08.630Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -533,7 +565,7 @@
"from": "10.1.5",
"to": "10.1.5",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -541,7 +573,7 @@
"from": "2.32.0",
"to": "2.32.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -549,7 +581,7 @@
"from": "6.10.2",
"to": "6.10.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -557,7 +589,7 @@
"from": "11.1.0",
"to": "11.1.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -565,7 +597,7 @@
"from": "5.5.1",
"to": "5.5.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -573,7 +605,7 @@
"from": "7.37.5",
"to": "7.37.5",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -581,7 +613,7 @@
"from": "4.6.2",
"to": "4.6.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -589,7 +621,7 @@
"from": "3.0.1",
"to": "3.0.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -597,7 +629,7 @@
"from": "12.1.1",
"to": "12.1.1",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -605,7 +637,7 @@
"from": "3.2.0",
"to": "3.2.0",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -613,7 +645,7 @@
"from": "9.1.7",
"to": "9.1.7",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -621,7 +653,7 @@
"from": "15.5.2",
"to": "15.5.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -629,7 +661,7 @@
"from": "8.4.38",
"to": "8.4.38",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -637,15 +669,23 @@
"from": "3.6.2",
"to": "3.6.2",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "prettier-plugin-tailwindcss",
"from": "0.6.13",
"from": "0.6.14",
"to": "0.6.14",
"strategy": "installed",
"generatedAt": "2025-09-24T13:59:11.231Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "shadcn",
"from": "3.4.1",
"to": "3.4.1",
"strategy": "installed",
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -653,15 +693,15 @@
"from": "0.1.20",
"to": "0.1.20",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
"name": "tailwindcss",
"from": "3.4.3",
"from": "4.1.13",
"to": "4.1.13",
"strategy": "installed",
"generatedAt": "2025-09-24T13:59:11.231Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
},
{
"section": "devDependencies",
@@ -669,6 +709,6 @@
"from": "5.5.4",
"to": "5.5.4",
"strategy": "installed",
"generatedAt": "2025-09-10T11:50:17.554Z"
"generatedAt": "2025-10-22T12:36:37.962Z"
}
]

1740
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,7 @@
"alert": "6.0.2",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"d3": "7.9.0",
"date-fns": "4.1.0",
"framer-motion": "11.18.2",
"intl-messageformat": "10.7.16",
@@ -72,17 +73,21 @@
"sharp": "0.33.5",
"tailwind-merge": "3.3.1",
"tailwindcss-animate": "1.0.7",
"topojson-client": "3.1.0",
"tw-animate-css": "1.4.0",
"uuid": "11.1.0",
"world-atlas": "2.0.2",
"zod": "4.1.11",
"zustand": "5.0.8"
},
"devDependencies": {
"@iconify/react": "5.2.1",
"@types/d3": "7.4.3",
"@playwright/test": "1.56.1",
"@types/node": "20.5.7",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@types/topojson-client": "3.1.5",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",

View File

@@ -1,8 +1,59 @@
@import "tailwindcss";
@config "../tailwind.config.js";
@theme {
/* Chart Severity Colors - Dark Theme */
--chart-info: #2e51b2;
--chart-warning: #fdd34f;
--chart-warning-emphasis: #ff7d19;
--chart-danger: #ff3077;
--chart-danger-emphasis: #971348;
/* Chart Status Colors */
--chart-success-color: #86da26;
--chart-fail: #db2b49;
/* Chart Radar Colors */
--chart-radar-primary: #b51c80;
--chart-radar-primary-rgb: 181 28 128;
/* Chart Provider Colors */
--chart-provider-aws: #ff9900;
--chart-provider-azure: #00bcd4;
--chart-provider-google: #EA4335;
/* Chart UI Colors - Dark Theme (defaults) */
--chart-text-primary: #ffffff;
--chart-text-secondary: #94a3b8;
--chart-border: #475569;
--chart-border-emphasis: #334155;
--chart-background: #1e293b;
/* Chart Alert Colors */
--chart-alert-bg: #432232;
--chart-alert-text: #f54280;
}
@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-success-color: #16a34a;
--chart-fail: #dc2626;
--chart-radar-primary: #9d174d;
--chart-text-primary: #1f2937;
--chart-text-secondary: #6b7280;
--chart-border: #d1d5db;
--chart-border-emphasis: #9ca3af;
--chart-background: #f9fafb;
--chart-alert-bg: #fecdd3;
--chart-alert-text: #be123c;
/* Chart HSL values */
--chart-success: 146 80% 35%;
--chart-fail: 339 90% 51%;
--chart-muted: 45 93% 47%;
@@ -17,6 +68,24 @@
}
.dark {
/* Dark Theme Chart Colors */
--chart-info: #2e51b2;
--chart-warning: #fdd34f;
--chart-warning-emphasis: #ff7d19;
--chart-danger: #ff3077;
--chart-danger-emphasis: #971348;
--chart-success-color: #86da26;
--chart-fail: #db2b49;
--chart-radar-primary: #b51c80;
--chart-text-primary: #ffffff;
--chart-text-secondary: #94a3b8;
--chart-border: #475569;
--chart-border-emphasis: #334155;
--chart-background: #1e293b;
--chart-alert-bg: #432232;
--chart-alert-text: #f54280;
/* Chart HSL values */
--chart-success: 146 80% 35%;
--chart-fail: 339 90% 51%;
--chart-muted: 45 93% 47%;
@@ -56,6 +125,7 @@
transform-box: fill-box;
transform-origin: center;
}
}
@layer base {